1use anyhow::Result;
2use std::sync::Arc;
3use std::time::Duration;
4use tokio::sync::RwLock;
5use tracing::{debug, warn};
6
7use super::raft_node::RaftCluster;
8use super::raft_sm::StateMachineData;
9use tatara_core::cluster::types::{
10 ClusterCommand, ClusterResponse, JobVersionEntry, NodeId, NodeMeta,
11};
12use tatara_core::domain::allocation::{Allocation, AllocationState, TaskState};
13use tatara_core::domain::event::{Event, EventKind};
14use tatara_core::domain::job::{Job, JobStatus};
15use tatara_core::domain::release::{Release, ReleaseStatus};
16use tatara_core::domain::source::{Source, SourceStatus};
17
18pub struct ClusterStore {
25 raft: Arc<RaftCluster>,
26 state: Arc<RwLock<StateMachineData>>,
28 propagation_timeout: Duration,
30}
31
32#[derive(Debug)]
34pub struct WriteResult<T> {
35 pub value: T,
36 pub log_index: u64,
38 pub fully_propagated: bool,
40 pub propagated_count: usize,
42 pub total_nodes: usize,
44}
45
46impl ClusterStore {
47 pub fn new(raft: Arc<RaftCluster>) -> Self {
48 let state = raft.read_local_sync();
49
50 Self {
51 raft,
52 state,
53 propagation_timeout: Duration::from_secs(10),
54 }
55 }
56
57 pub fn with_propagation_timeout(mut self, timeout: Duration) -> Self {
59 self.propagation_timeout = timeout;
60 self
61 }
62
63 pub async fn get_job(&self, id: &str) -> Option<Job> {
66 let data = self.state.read().await;
67 data.cluster_state.jobs.get(id).cloned()
68 }
69
70 pub async fn list_jobs(&self) -> Vec<Job> {
71 let data = self.state.read().await;
72 data.cluster_state.jobs.values().cloned().collect()
73 }
74
75 pub async fn get_allocation(&self, id: &uuid::Uuid) -> Option<Allocation> {
76 let data = self.state.read().await;
77 data.cluster_state.allocations.get(id).cloned()
78 }
79
80 pub async fn list_allocations(&self) -> Vec<Allocation> {
81 let data = self.state.read().await;
82 data.cluster_state.allocations.values().cloned().collect()
83 }
84
85 pub async fn list_allocations_for_job(&self, job_id: &str) -> Vec<Allocation> {
86 let data = self.state.read().await;
87 data.cluster_state
88 .allocations
89 .values()
90 .filter(|a| a.job_id == job_id)
91 .cloned()
92 .collect()
93 }
94
95 pub async fn get_node_meta(&self, id: &NodeId) -> Option<NodeMeta> {
96 let data = self.state.read().await;
97 data.cluster_state.nodes.get(id).cloned()
98 }
99
100 pub async fn list_nodes(&self) -> Vec<NodeMeta> {
101 let data = self.state.read().await;
102 data.cluster_state.nodes.values().cloned().collect()
103 }
104
105 pub async fn get_job_history(&self, job_id: &str) -> Vec<JobVersionEntry> {
107 let data = self.state.read().await;
108 data.cluster_state
109 .job_history
110 .get(job_id)
111 .cloned()
112 .unwrap_or_default()
113 }
114
115 pub async fn list_events(
117 &self,
118 kind: Option<&EventKind>,
119 since: Option<chrono::DateTime<chrono::Utc>>,
120 ) -> Vec<Event> {
121 let data = self.state.read().await;
122 data.cluster_state
123 .events
124 .query(kind, since)
125 .into_iter()
126 .cloned()
127 .collect()
128 }
129
130 pub async fn list_releases(&self) -> Vec<Release> {
132 let data = self.state.read().await;
133 data.cluster_state.releases.values().cloned().collect()
134 }
135
136 pub async fn get_release(&self, id: &uuid::Uuid) -> Option<Release> {
138 let data = self.state.read().await;
139 data.cluster_state.releases.get(id).cloned()
140 }
141
142 pub async fn list_sources(&self) -> Vec<Source> {
144 let data = self.state.read().await;
145 data.cluster_state.sources.values().cloned().collect()
146 }
147
148 pub async fn get_source(&self, id: &uuid::Uuid) -> Option<Source> {
150 let data = self.state.read().await;
151 data.cluster_state.sources.get(id).cloned()
152 }
153
154 pub async fn get_source_by_name(&self, name: &str) -> Option<Source> {
156 let data = self.state.read().await;
157 data.cluster_state
158 .sources
159 .values()
160 .find(|s| s.name == name)
161 .cloned()
162 }
163
164 pub async fn state(&self) -> tatara_core::cluster::types::ClusterState {
166 let data = self.state.read().await;
167 data.cluster_state.clone()
168 }
169
170 pub async fn is_leader(&self) -> bool {
172 self.raft.is_leader().await
173 }
174
175 pub async fn get_job_linearizable(&self, id: &str) -> Result<Option<Job>> {
178 let state = self.raft.read_state().await?;
179 let data = state.read().await;
180 Ok(data.cluster_state.jobs.get(id).cloned())
181 }
182
183 pub async fn put_job(&self, job: Job) -> Result<WriteResult<Job>> {
187 let resp = self.raft.write(ClusterCommand::PutJob(job)).await?;
188 let job = match resp {
189 ClusterResponse::Job(j) => j,
190 ClusterResponse::Error(e) => anyhow::bail!("Failed to put job: {}", e),
191 _ => anyhow::bail!("Unexpected response from Raft"),
192 };
193
194 let log_index = self.current_commit_index().await;
195 let prop = self.await_propagation(log_index).await;
196
197 Ok(WriteResult {
198 value: job,
199 log_index,
200 fully_propagated: prop.fully_propagated,
201 propagated_count: prop.propagated_count,
202 total_nodes: prop.total_nodes,
203 })
204 }
205
206 pub async fn update_job_status(
208 &self,
209 job_id: &str,
210 status: JobStatus,
211 ) -> Result<WriteResult<Job>> {
212 let resp = self
213 .raft
214 .write(ClusterCommand::UpdateJobStatus {
215 job_id: job_id.to_string(),
216 status,
217 })
218 .await?;
219
220 let job = match resp {
221 ClusterResponse::Job(j) => j,
222 ClusterResponse::Error(e) => anyhow::bail!("Failed to update job: {}", e),
223 _ => anyhow::bail!("Unexpected response from Raft"),
224 };
225
226 let log_index = self.current_commit_index().await;
227 let prop = self.await_propagation(log_index).await;
228
229 Ok(WriteResult {
230 value: job,
231 log_index,
232 fully_propagated: prop.fully_propagated,
233 propagated_count: prop.propagated_count,
234 total_nodes: prop.total_nodes,
235 })
236 }
237
238 pub async fn put_allocation(&self, alloc: Allocation) -> Result<WriteResult<Allocation>> {
240 let resp = self
241 .raft
242 .write(ClusterCommand::PutAllocation(alloc))
243 .await?;
244
245 let alloc = match resp {
246 ClusterResponse::Allocation(a) => a,
247 ClusterResponse::Error(e) => anyhow::bail!("Failed to put allocation: {}", e),
248 _ => anyhow::bail!("Unexpected response from Raft"),
249 };
250
251 let log_index = self.current_commit_index().await;
252 let prop = self.await_propagation(log_index).await;
253
254 Ok(WriteResult {
255 value: alloc,
256 log_index,
257 fully_propagated: prop.fully_propagated,
258 propagated_count: prop.propagated_count,
259 total_nodes: prop.total_nodes,
260 })
261 }
262
263 pub async fn update_allocation_state(
265 &self,
266 alloc_id: uuid::Uuid,
267 state: AllocationState,
268 task_states: std::collections::HashMap<String, TaskState>,
269 ) -> Result<WriteResult<Allocation>> {
270 let resp = self
271 .raft
272 .write(ClusterCommand::UpdateAllocation {
273 alloc_id,
274 state,
275 task_states,
276 })
277 .await?;
278
279 let alloc = match resp {
280 ClusterResponse::Allocation(a) => a,
281 ClusterResponse::Error(e) => anyhow::bail!("Failed to update allocation: {}", e),
282 _ => anyhow::bail!("Unexpected response from Raft"),
283 };
284
285 let log_index = self.current_commit_index().await;
286 let prop = self.await_propagation(log_index).await;
287
288 Ok(WriteResult {
289 value: alloc,
290 log_index,
291 fully_propagated: prop.fully_propagated,
292 propagated_count: prop.propagated_count,
293 total_nodes: prop.total_nodes,
294 })
295 }
296
297 pub async fn register_node(&self, meta: NodeMeta) -> Result<WriteResult<()>> {
299 self.raft.write(ClusterCommand::RegisterNode(meta)).await?;
300
301 let log_index = self.current_commit_index().await;
302 let prop = self.await_propagation(log_index).await;
303
304 Ok(WriteResult {
305 value: (),
306 log_index,
307 fully_propagated: prop.fully_propagated,
308 propagated_count: prop.propagated_count,
309 total_nodes: prop.total_nodes,
310 })
311 }
312
313 pub async fn advertise_chunk(&self, hash: String, node_id: NodeId) -> Result<()> {
315 self.raft
316 .write(ClusterCommand::AdvertiseChunk { hash, node_id })
317 .await?;
318 Ok(())
319 }
320
321 pub async fn emit_event(&self, event: Event) -> Result<()> {
323 self.raft.write(ClusterCommand::EmitEvent(event)).await?;
324 Ok(())
325 }
326
327 pub async fn rollback_job(&self, job_id: &str, version: u64) -> Result<WriteResult<Job>> {
329 let resp = self
330 .raft
331 .write(ClusterCommand::RollbackJob {
332 job_id: job_id.to_string(),
333 version,
334 })
335 .await?;
336
337 let job = match resp {
338 ClusterResponse::Job(j) => j,
339 ClusterResponse::Error(e) => anyhow::bail!("{}", e),
340 _ => anyhow::bail!("Unexpected response from Raft"),
341 };
342
343 let log_index = self.current_commit_index().await;
344 let prop = self.await_propagation(log_index).await;
345
346 Ok(WriteResult {
347 value: job,
348 log_index,
349 fully_propagated: prop.fully_propagated,
350 propagated_count: prop.propagated_count,
351 total_nodes: prop.total_nodes,
352 })
353 }
354
355 pub async fn put_release(&self, release: Release) -> Result<WriteResult<Release>> {
357 let resp = self.raft.write(ClusterCommand::PutRelease(release)).await?;
358
359 let release = match resp {
360 ClusterResponse::Release(r) => r,
361 ClusterResponse::Error(e) => anyhow::bail!("Failed to put release: {}", e),
362 _ => anyhow::bail!("Unexpected response from Raft"),
363 };
364
365 let log_index = self.current_commit_index().await;
366 let prop = self.await_propagation(log_index).await;
367
368 Ok(WriteResult {
369 value: release,
370 log_index,
371 fully_propagated: prop.fully_propagated,
372 propagated_count: prop.propagated_count,
373 total_nodes: prop.total_nodes,
374 })
375 }
376
377 pub async fn update_release_status(
379 &self,
380 release_id: uuid::Uuid,
381 status: ReleaseStatus,
382 ) -> Result<WriteResult<Release>> {
383 let resp = self
384 .raft
385 .write(ClusterCommand::UpdateReleaseStatus { release_id, status })
386 .await?;
387
388 let release = match resp {
389 ClusterResponse::Release(r) => r,
390 ClusterResponse::Error(e) => anyhow::bail!("{}", e),
391 _ => anyhow::bail!("Unexpected response from Raft"),
392 };
393
394 let log_index = self.current_commit_index().await;
395 let prop = self.await_propagation(log_index).await;
396
397 Ok(WriteResult {
398 value: release,
399 log_index,
400 fully_propagated: prop.fully_propagated,
401 propagated_count: prop.propagated_count,
402 total_nodes: prop.total_nodes,
403 })
404 }
405
406 pub async fn drain_node(&self, node_id: NodeId) -> Result<()> {
408 let resp = self
409 .raft
410 .write(ClusterCommand::DrainNode { node_id })
411 .await?;
412 match resp {
413 ClusterResponse::Ok => Ok(()),
414 ClusterResponse::Error(e) => anyhow::bail!("{}", e),
415 _ => Ok(()),
416 }
417 }
418
419 pub async fn set_node_eligibility(&self, node_id: NodeId, eligible: bool) -> Result<()> {
421 let resp = self
422 .raft
423 .write(ClusterCommand::SetNodeEligibility { node_id, eligible })
424 .await?;
425 match resp {
426 ClusterResponse::Ok => Ok(()),
427 ClusterResponse::Error(e) => anyhow::bail!("{}", e),
428 _ => Ok(()),
429 }
430 }
431
432 pub async fn put_source(&self, source: Source) -> Result<WriteResult<Source>> {
434 let resp = self.raft.write(ClusterCommand::PutSource(source)).await?;
435
436 let source = match resp {
437 ClusterResponse::Source(s) => s,
438 ClusterResponse::Error(e) => anyhow::bail!("Failed to put source: {}", e),
439 _ => anyhow::bail!("Unexpected response from Raft"),
440 };
441
442 let log_index = self.current_commit_index().await;
443 let prop = self.await_propagation(log_index).await;
444
445 Ok(WriteResult {
446 value: source,
447 log_index,
448 fully_propagated: prop.fully_propagated,
449 propagated_count: prop.propagated_count,
450 total_nodes: prop.total_nodes,
451 })
452 }
453
454 pub async fn update_source(
456 &self,
457 source_id: uuid::Uuid,
458 status: SourceStatus,
459 last_rev: Option<String>,
460 last_error: Option<String>,
461 managed_jobs: Option<std::collections::HashMap<String, String>>,
462 ) -> Result<WriteResult<Source>> {
463 let resp = self
464 .raft
465 .write(ClusterCommand::UpdateSource {
466 source_id,
467 status,
468 last_rev,
469 last_error,
470 managed_jobs,
471 })
472 .await?;
473
474 let source = match resp {
475 ClusterResponse::Source(s) => s,
476 ClusterResponse::Error(e) => anyhow::bail!("{}", e),
477 _ => anyhow::bail!("Unexpected response from Raft"),
478 };
479
480 let log_index = self.current_commit_index().await;
481 let prop = self.await_propagation(log_index).await;
482
483 Ok(WriteResult {
484 value: source,
485 log_index,
486 fully_propagated: prop.fully_propagated,
487 propagated_count: prop.propagated_count,
488 total_nodes: prop.total_nodes,
489 })
490 }
491
492 pub async fn delete_source(&self, source_id: uuid::Uuid) -> Result<()> {
494 let resp = self
495 .raft
496 .write(ClusterCommand::DeleteSource { source_id })
497 .await?;
498 match resp {
499 ClusterResponse::Ok => Ok(()),
500 ClusterResponse::Error(e) => anyhow::bail!("{}", e),
501 _ => Ok(()),
502 }
503 }
504
505 async fn current_commit_index(&self) -> u64 {
509 let metrics = self.raft.raft.metrics().borrow().clone();
510 metrics.last_applied.map(|l| l.index).unwrap_or(0)
511 }
512
513 async fn await_propagation(&self, target_index: u64) -> PropagationStatus {
518 let deadline = tokio::time::Instant::now() + self.propagation_timeout;
519 let mut interval = tokio::time::interval(Duration::from_millis(100));
520
521 loop {
522 interval.tick().await;
523
524 let status = self.check_propagation(target_index).await;
525
526 if status.fully_propagated {
527 debug!(
528 log_index = target_index,
529 nodes = status.total_nodes,
530 "Write fully propagated to all nodes"
531 );
532 return status;
533 }
534
535 if tokio::time::Instant::now() >= deadline {
536 warn!(
537 log_index = target_index,
538 propagated = status.propagated_count,
539 total = status.total_nodes,
540 "Write propagation timed out — not all nodes confirmed"
541 );
542 return status;
543 }
544 }
545 }
546
547 async fn check_propagation(&self, target_index: u64) -> PropagationStatus {
549 let metrics = self.raft.raft.metrics().borrow().clone();
550
551 let replication = metrics.replication;
553
554 match replication {
555 Some(ref rep) => {
556 let total = rep.len() + 1; let leader_applied = metrics.last_applied.map(|l| l.index).unwrap_or(0);
558
559 let mut propagated = if leader_applied >= target_index {
560 1 } else {
562 0
563 };
564
565 for (_node_id, log_id_opt) in rep.iter() {
566 if let Some(log_id) = log_id_opt {
567 if log_id.index >= target_index {
568 propagated += 1;
569 }
570 }
571 }
572
573 PropagationStatus {
574 fully_propagated: propagated >= total,
575 propagated_count: propagated,
576 total_nodes: total,
577 }
578 }
579 None => {
580 PropagationStatus {
582 fully_propagated: true,
583 propagated_count: 1,
584 total_nodes: 1,
585 }
586 }
587 }
588 }
589}
590
591struct PropagationStatus {
592 fully_propagated: bool,
593 propagated_count: usize,
594 total_nodes: usize,
595}