Skip to main content

tatara_engine/client/
executor.rs

1use anyhow::{Context, Result};
2use std::collections::HashMap;
3use std::path::PathBuf;
4use std::sync::Arc;
5use std::time::Duration;
6use tokio::sync::RwLock;
7use tracing::{error, info, warn};
8use uuid::Uuid;
9
10use crate::cluster::store::ClusterStore;
11use crate::domain::state_store::StateStore;
12use crate::drivers::{DriverRegistry, TaskHandle};
13use tatara_core::domain::allocation::{Allocation, AllocationState, TaskRunState};
14
15struct RunningTask {
16    handle: TaskHandle,
17    task_name: String,
18    alloc_id: Uuid,
19}
20
21pub struct Executor {
22    store: Arc<StateStore>,
23    drivers: Arc<DriverRegistry>,
24    alloc_dir: PathBuf,
25    running: RwLock<HashMap<Uuid, Vec<RunningTask>>>,
26    /// Optional Raft-backed store for reporting observations to the cluster.
27    /// When set, state changes are reported through Raft for cluster-wide visibility.
28    cluster_store: Option<Arc<ClusterStore>>,
29}
30
31impl Executor {
32    pub fn new(store: Arc<StateStore>, drivers: Arc<DriverRegistry>, alloc_dir: PathBuf) -> Self {
33        Self {
34            store,
35            drivers,
36            alloc_dir,
37            running: RwLock::new(HashMap::new()),
38            cluster_store: None,
39        }
40    }
41
42    /// Set the cluster store for Raft-backed observation reporting.
43    pub fn with_cluster_store(mut self, cluster_store: Arc<ClusterStore>) -> Self {
44        self.cluster_store = Some(cluster_store);
45        self
46    }
47
48    /// Report an allocation state change through Raft (if cluster store is wired).
49    async fn report_to_cluster(&self, alloc_id: Uuid, state: AllocationState) {
50        if let Some(ref cs) = self.cluster_store {
51            if let Err(e) = cs
52                .update_allocation_state(alloc_id, state.clone(), HashMap::new())
53                .await
54            {
55                warn!(
56                    alloc_id = %alloc_id,
57                    state = ?state,
58                    error = %e,
59                    "failed to report observation to cluster"
60                );
61            }
62        }
63    }
64
65    pub async fn start_allocation(&self, alloc: Allocation) -> Result<()> {
66        let alloc_id = alloc.id;
67        let job = self
68            .store
69            .get_job(&alloc.job_id)
70            .await
71            .context("Job not found for allocation")?;
72
73        let group = job
74            .groups
75            .iter()
76            .find(|g| g.name == alloc.group_name)
77            .context("Task group not found in job")?;
78
79        let alloc_path = self.alloc_dir.join(alloc_id.to_string());
80        tokio::fs::create_dir_all(&alloc_path).await?;
81
82        let mut tasks = Vec::new();
83
84        for task in &group.tasks {
85            let driver = self
86                .drivers
87                .get(&task.driver)
88                .with_context(|| format!("Driver {:?} not available", task.driver))?;
89
90            match driver.start(task, &alloc_path).await {
91                Ok(handle) => {
92                    info!(
93                        alloc_id = %alloc_id,
94                        task = %task.name,
95                        driver = %driver.name(),
96                        pid = ?handle.pid,
97                        "Task started"
98                    );
99
100                    // Update task state to running
101                    self.store
102                        .update_allocation(&alloc_id, |a| {
103                            if let Some(ts) = a.task_states.get_mut(&task.name) {
104                                ts.state = TaskRunState::Running;
105                                ts.pid = handle.pid;
106                                ts.started_at = Some(handle.started_at);
107                            }
108                        })
109                        .await?;
110
111                    tasks.push(RunningTask {
112                        handle,
113                        task_name: task.name.clone(),
114                        alloc_id,
115                    });
116                }
117                Err(e) => {
118                    error!(
119                        alloc_id = %alloc_id,
120                        task = %task.name,
121                        error = %e,
122                        "Failed to start task"
123                    );
124
125                    self.store
126                        .update_allocation(&alloc_id, |a| {
127                            if let Some(ts) = a.task_states.get_mut(&task.name) {
128                                ts.state = TaskRunState::Dead;
129                            }
130                            a.state = AllocationState::Failed;
131                        })
132                        .await?;
133
134                    return Err(e);
135                }
136            }
137        }
138
139        // Mark allocation as running
140        self.store
141            .update_allocation(&alloc_id, |a| {
142                a.state = AllocationState::Running;
143            })
144            .await?;
145
146        // Report to cluster for distributed visibility
147        self.report_to_cluster(alloc_id, AllocationState::Running)
148            .await;
149
150        self.running.write().await.insert(alloc_id, tasks);
151
152        Ok(())
153    }
154
155    pub async fn stop_allocation(&self, alloc_id: &Uuid, timeout: Duration) -> Result<()> {
156        let mut running = self.running.write().await;
157
158        if let Some(tasks) = running.remove(alloc_id) {
159            for rt in &tasks {
160                let driver = self
161                    .drivers
162                    .get(&rt.handle.driver)
163                    .context("Driver not found")?;
164
165                if let Err(e) = driver.stop(&rt.handle, timeout).await {
166                    warn!(
167                        alloc_id = %alloc_id,
168                        task = %rt.task_name,
169                        error = %e,
170                        "Failed to stop task"
171                    );
172                }
173            }
174        }
175
176        self.store
177            .update_allocation(alloc_id, |a| {
178                a.state = AllocationState::Complete;
179                for ts in a.task_states.values_mut() {
180                    ts.state = TaskRunState::Dead;
181                    ts.finished_at = Some(chrono::Utc::now());
182                }
183            })
184            .await?;
185
186        // Report completion to cluster
187        self.report_to_cluster(*alloc_id, AllocationState::Complete)
188            .await;
189
190        Ok(())
191    }
192
193    /// Check health of all running allocations. Returns dead allocations.
194    pub async fn check_health(&self) -> Vec<Uuid> {
195        let running = self.running.read().await;
196        let mut dead = Vec::new();
197
198        for (alloc_id, tasks) in running.iter() {
199            let mut all_dead = true;
200
201            for rt in tasks {
202                if let Some(driver) = self.drivers.get(&rt.handle.driver) {
203                    match driver.status(&rt.handle).await {
204                        Ok(TaskRunState::Running) => {
205                            all_dead = false;
206                        }
207                        Ok(TaskRunState::Dead) => {
208                            let _ = self
209                                .store
210                                .update_allocation(alloc_id, |a| {
211                                    if let Some(ts) = a.task_states.get_mut(&rt.task_name) {
212                                        ts.state = TaskRunState::Dead;
213                                        ts.finished_at = Some(chrono::Utc::now());
214                                    }
215                                })
216                                .await;
217                        }
218                        _ => {}
219                    }
220                }
221            }
222
223            if all_dead {
224                dead.push(*alloc_id);
225            }
226        }
227
228        dead
229    }
230
231    /// Check health of all running allocations, returning per-task status.
232    pub async fn check_task_health_detailed(&self) -> HashMap<Uuid, Vec<(String, TaskRunState)>> {
233        let running = self.running.read().await;
234        let mut result: HashMap<Uuid, Vec<(String, TaskRunState)>> = HashMap::new();
235
236        for (alloc_id, tasks) in running.iter() {
237            let mut task_states = Vec::new();
238
239            for rt in tasks {
240                let state = if let Some(driver) = self.drivers.get(&rt.handle.driver) {
241                    match driver.status(&rt.handle).await {
242                        Ok(s) => s,
243                        Err(_) => TaskRunState::Dead,
244                    }
245                } else {
246                    TaskRunState::Dead
247                };
248                task_states.push((rt.task_name.clone(), state));
249            }
250
251            result.insert(*alloc_id, task_states);
252        }
253
254        result
255    }
256
257    /// Restart a single task within a running allocation.
258    pub async fn restart_task(&self, alloc_id: &Uuid, task_name: &str) -> Result<()> {
259        let alloc = self
260            .store
261            .get_allocation(alloc_id)
262            .await
263            .context("Allocation not found")?;
264
265        let job = self
266            .store
267            .get_job(&alloc.job_id)
268            .await
269            .context("Job not found for allocation")?;
270
271        let group = job
272            .groups
273            .iter()
274            .find(|g| g.name == alloc.group_name)
275            .context("Task group not found in job")?;
276
277        let task = group
278            .tasks
279            .iter()
280            .find(|t| t.name == task_name)
281            .with_context(|| format!("Task {} not found in group {}", task_name, group.name))?;
282
283        // Stop the old process
284        {
285            let mut running = self.running.write().await;
286            if let Some(tasks) = running.get_mut(alloc_id) {
287                if let Some(rt) = tasks.iter().find(|t| t.task_name == task_name) {
288                    if let Some(driver) = self.drivers.get(&rt.handle.driver) {
289                        let _ = driver.stop(&rt.handle, Duration::from_secs(10)).await;
290                    }
291                }
292                tasks.retain(|t| t.task_name != task_name);
293            }
294        }
295
296        // Start the task fresh
297        let alloc_path = self.alloc_dir.join(alloc_id.to_string());
298        tokio::fs::create_dir_all(&alloc_path).await?;
299
300        let driver = self
301            .drivers
302            .get(&task.driver)
303            .with_context(|| format!("Driver {:?} not available", task.driver))?;
304
305        let handle = driver.start(task, &alloc_path).await?;
306
307        info!(
308            alloc_id = %alloc_id,
309            task = %task_name,
310            pid = ?handle.pid,
311            "Task restarted"
312        );
313
314        // Update task state in store
315        self.store
316            .update_allocation(alloc_id, |a| {
317                if let Some(ts) = a.task_states.get_mut(task_name) {
318                    ts.state = TaskRunState::Running;
319                    ts.pid = handle.pid;
320                    ts.started_at = Some(handle.started_at);
321                    ts.restarts += 1;
322                }
323            })
324            .await?;
325
326        // Re-add to running map
327        self.running
328            .write()
329            .await
330            .entry(*alloc_id)
331            .or_default()
332            .push(RunningTask {
333                handle,
334                task_name: task_name.to_string(),
335                alloc_id: *alloc_id,
336            });
337
338        Ok(())
339    }
340
341    pub async fn get_task_handle(&self, alloc_id: &Uuid, task_name: &str) -> Option<TaskHandle> {
342        let running = self.running.read().await;
343        running.get(alloc_id).and_then(|tasks| {
344            tasks
345                .iter()
346                .find(|t| t.task_name == task_name)
347                .map(|t| t.handle.clone())
348        })
349    }
350}