tatara_engine/client/
executor.rs1use 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 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 pub fn with_cluster_store(mut self, cluster_store: Arc<ClusterStore>) -> Self {
44 self.cluster_store = Some(cluster_store);
45 self
46 }
47
48 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 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 self.store
141 .update_allocation(&alloc_id, |a| {
142 a.state = AllocationState::Running;
143 })
144 .await?;
145
146 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 self.report_to_cluster(*alloc_id, AllocationState::Complete)
188 .await;
189
190 Ok(())
191 }
192
193 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 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 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 {
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 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 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 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}