Skip to main content

tatara_engine/domain/
state_store.rs

1use 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}