tatara_engine/domain/
state_store.rs1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5use tokio::sync::RwLock;
6use uuid::Uuid;
7
8use tatara_core::domain::allocation::Allocation;
9use tatara_core::domain::job::Job;
10use tatara_core::domain::node::Node;
11use tatara_core::domain::source::Source;
12
13#[derive(Debug, Serialize, Deserialize, Default)]
14struct StateSnapshot {
15 jobs: HashMap<String, Job>,
16 allocations: HashMap<Uuid, Allocation>,
17 nodes: HashMap<String, Node>,
18 #[serde(default)]
19 sources: HashMap<Uuid, Source>,
20}
21
22pub struct StateStore {
23 dir: PathBuf,
24 state: RwLock<StateSnapshot>,
25}
26
27impl StateStore {
28 pub async fn new(dir: &Path) -> Result<Self> {
29 tokio::fs::create_dir_all(dir)
30 .await
31 .context("Failed to create state directory")?;
32
33 let state_file = dir.join("state.json");
34 let state = if state_file.exists() {
35 let data = tokio::fs::read_to_string(&state_file)
36 .await
37 .context("Failed to read state file")?;
38 serde_json::from_str(&data).unwrap_or_default()
39 } else {
40 StateSnapshot::default()
41 };
42
43 Ok(Self {
44 dir: dir.to_path_buf(),
45 state: RwLock::new(state),
46 })
47 }
48
49 pub async fn put_job(&self, job: Job) -> Result<()> {
50 let mut state = self.state.write().await;
51 state.jobs.insert(job.id.clone(), job);
52 self.persist(&state).await
53 }
54
55 pub async fn get_job(&self, id: &str) -> Option<Job> {
56 let state = self.state.read().await;
57 state.jobs.get(id).cloned()
58 }
59
60 pub async fn list_jobs(&self) -> Vec<Job> {
61 let state = self.state.read().await;
62 state.jobs.values().cloned().collect()
63 }
64
65 pub async fn update_job<F>(&self, id: &str, f: F) -> Result<Option<Job>>
66 where
67 F: FnOnce(&mut Job),
68 {
69 let mut state = self.state.write().await;
70 if let Some(job) = state.jobs.get_mut(id) {
71 f(job);
72 let job = job.clone();
73 self.persist(&state).await?;
74 Ok(Some(job))
75 } else {
76 Ok(None)
77 }
78 }
79
80 pub async fn put_allocation(&self, alloc: Allocation) -> Result<()> {
81 let mut state = self.state.write().await;
82 state.allocations.insert(alloc.id, alloc);
83 self.persist(&state).await
84 }
85
86 pub async fn get_allocation(&self, id: &Uuid) -> Option<Allocation> {
87 let state = self.state.read().await;
88 state.allocations.get(id).cloned()
89 }
90
91 pub async fn list_allocations(&self) -> Vec<Allocation> {
92 let state = self.state.read().await;
93 state.allocations.values().cloned().collect()
94 }
95
96 pub async fn list_allocations_for_job(&self, job_id: &str) -> Vec<Allocation> {
97 let state = self.state.read().await;
98 state
99 .allocations
100 .values()
101 .filter(|a| a.job_id == job_id)
102 .cloned()
103 .collect()
104 }
105
106 pub async fn update_allocation<F>(&self, id: &Uuid, f: F) -> Result<Option<Allocation>>
107 where
108 F: FnOnce(&mut Allocation),
109 {
110 let mut state = self.state.write().await;
111 if let Some(alloc) = state.allocations.get_mut(id) {
112 f(alloc);
113 let alloc = alloc.clone();
114 self.persist(&state).await?;
115 Ok(Some(alloc))
116 } else {
117 Ok(None)
118 }
119 }
120
121 pub async fn put_node(&self, node: Node) -> Result<()> {
122 let mut state = self.state.write().await;
123 state.nodes.insert(node.id.clone(), node);
124 self.persist(&state).await
125 }
126
127 pub async fn get_node(&self, id: &str) -> Option<Node> {
128 let state = self.state.read().await;
129 state.nodes.get(id).cloned()
130 }
131
132 pub async fn list_nodes(&self) -> Vec<Node> {
133 let state = self.state.read().await;
134 state.nodes.values().cloned().collect()
135 }
136
137 pub async fn put_source(&self, source: Source) -> Result<()> {
138 let mut state = self.state.write().await;
139 state.sources.insert(source.id, source);
140 self.persist(&state).await
141 }
142
143 pub async fn get_source(&self, id: &Uuid) -> Option<Source> {
144 let state = self.state.read().await;
145 state.sources.get(id).cloned()
146 }
147
148 pub async fn get_source_by_name(&self, name: &str) -> Option<Source> {
149 let state = self.state.read().await;
150 state.sources.values().find(|s| s.name == name).cloned()
151 }
152
153 pub async fn list_sources(&self) -> Vec<Source> {
154 let state = self.state.read().await;
155 state.sources.values().cloned().collect()
156 }
157
158 pub async fn update_source<F>(&self, id: &Uuid, f: F) -> Result<Option<Source>>
159 where
160 F: FnOnce(&mut Source),
161 {
162 let mut state = self.state.write().await;
163 if let Some(source) = state.sources.get_mut(id) {
164 f(source);
165 let source = source.clone();
166 self.persist(&state).await?;
167 Ok(Some(source))
168 } else {
169 Ok(None)
170 }
171 }
172
173 pub async fn delete_source(&self, id: &Uuid) -> Result<()> {
174 let mut state = self.state.write().await;
175 state.sources.remove(id);
176 self.persist(&state).await
177 }
178
179 async fn persist(&self, state: &StateSnapshot) -> Result<()> {
180 let data = serde_json::to_string_pretty(state).context("Failed to serialize state")?;
181
182 let tmp = self.dir.join("state.json.tmp");
183 let target = self.dir.join("state.json");
184
185 tokio::fs::write(&tmp, &data)
186 .await
187 .context("Failed to write temporary state file")?;
188 tokio::fs::rename(&tmp, &target)
189 .await
190 .context("Failed to rename state file")?;
191
192 Ok(())
193 }
194}