kbolt_core/engine/
schedule_status_ops.rs1use crate::error::CoreError;
2use crate::schedule_backend::{current_schedule_backend, inspect_schedule_backend};
3use crate::schedule_state_store::ScheduleRunStateStore;
4use crate::schedule_store::ScheduleCatalog;
5use crate::schedule_support::schedule_id_sort_key;
6use crate::Result;
7use kbolt_types::{
8 KboltError, ScheduleOrphan, ScheduleScope, ScheduleState, ScheduleStatusEntry,
9 ScheduleStatusResponse,
10};
11
12use super::Engine;
13
14impl Engine {
15 pub fn schedule_status(&self) -> Result<ScheduleStatusResponse> {
16 let backend = current_schedule_backend()?;
17 let mut schedules = ScheduleCatalog::load(&self.config.config_dir)?.schedules;
18 schedules.sort_by_key(|schedule| schedule_id_sort_key(&schedule.id));
19
20 let inspection =
21 inspect_schedule_backend(&self.config.config_dir, &self.config.cache_dir, &schedules)?;
22 let mut entries = Vec::with_capacity(schedules.len());
23
24 for schedule in schedules {
25 let state = if self.schedule_targets_missing(&schedule.scope)? {
26 ScheduleState::TargetMissing
27 } else if inspection.drifted_ids.contains(&schedule.id) {
28 ScheduleState::Drifted
29 } else {
30 ScheduleState::Installed
31 };
32
33 let run_state = ScheduleRunStateStore::load(&self.config.cache_dir, &schedule.id)?;
34 entries.push(ScheduleStatusEntry {
35 schedule,
36 backend,
37 state,
38 run_state,
39 });
40 }
41
42 let orphans = inspection
43 .orphan_ids
44 .into_iter()
45 .map(|id| ScheduleOrphan { id, backend })
46 .collect();
47
48 Ok(ScheduleStatusResponse {
49 schedules: entries,
50 orphans,
51 })
52 }
53
54 fn schedule_targets_missing(&self, scope: &ScheduleScope) -> Result<bool> {
55 match scope {
56 ScheduleScope::All => Ok(false),
57 ScheduleScope::Space { space } => match self.storage.get_space(space) {
58 Ok(_) => Ok(false),
59 Err(CoreError::Domain(KboltError::SpaceNotFound { .. })) => Ok(true),
60 Err(err) => Err(err),
61 },
62 ScheduleScope::Collections { space, collections } => {
63 let resolved_space = match self.storage.get_space(space) {
64 Ok(space) => space,
65 Err(CoreError::Domain(KboltError::SpaceNotFound { .. })) => return Ok(true),
66 Err(err) => return Err(err),
67 };
68
69 for collection in collections {
70 match self.storage.get_collection(resolved_space.id, collection) {
71 Ok(_) => {}
72 Err(CoreError::Domain(KboltError::CollectionNotFound { .. })) => {
73 return Ok(true)
74 }
75 Err(err) => return Err(err),
76 }
77 }
78
79 Ok(false)
80 }
81 }
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use fs2::FileExt;
88 use std::fs::OpenOptions;
89 use std::mem;
90
91 use tempfile::tempdir;
92
93 use super::Engine;
94 use crate::config::{ChunkingConfig, Config, RankingConfig, ReapingConfig};
95 use crate::storage::Storage;
96 use kbolt_types::{AddScheduleRequest, ScheduleScope, ScheduleTrigger};
97
98 #[test]
99 fn schedule_status_succeeds_while_global_lock_is_held() {
100 let engine = test_engine();
101 engine
102 .add_schedule(AddScheduleRequest {
103 trigger: ScheduleTrigger::Daily {
104 time: "09:00".to_string(),
105 },
106 scope: ScheduleScope::All,
107 })
108 .expect("add schedule");
109
110 let lock_path = engine.config().cache_dir.join("kbolt.lock");
111 std::fs::create_dir_all(&engine.config().cache_dir).expect("create cache dir");
112 let holder = OpenOptions::new()
113 .read(true)
114 .write(true)
115 .create(true)
116 .truncate(false)
117 .open(&lock_path)
118 .expect("open lock file");
119 FileExt::try_lock_exclusive(&holder).expect("acquire global lock");
120
121 let status = engine.schedule_status().expect("load schedule status");
122 assert_eq!(status.schedules.len(), 1);
123 assert!(status.orphans.is_empty());
124 }
125
126 fn test_engine() -> Engine {
127 let root = tempdir().expect("create temp root");
128 let root_path = root.path().to_path_buf();
129 mem::forget(root);
130 let config_dir = root_path.join("config");
131 let cache_dir = root_path.join("cache");
132 let storage = Storage::new(&cache_dir).expect("create storage");
133 let config = Config {
134 config_dir,
135 cache_dir,
136 default_space: None,
137 providers: std::collections::HashMap::new(),
138 roles: crate::config::RoleBindingsConfig::default(),
139 reaping: ReapingConfig { days: 7 },
140 chunking: ChunkingConfig::default(),
141 ranking: RankingConfig::default(),
142 };
143 Engine::from_parts(storage, config)
144 }
145}