1use crate::error::{DistributedError, Result};
7use crate::flight::FlightClient;
8use crate::task::{PartitionId, Task, TaskId, TaskOperation, TaskResult, TaskScheduler};
9use crate::worker::WorkerStatus;
10use arrow::record_batch::RecordBatch;
11use std::collections::HashMap;
12use std::sync::{Arc, RwLock};
13use std::time::{Duration, Instant};
14use tokio::sync::mpsc;
15use tracing::{debug, error, info, warn};
16
17#[derive(Debug, Clone)]
19pub struct CoordinatorConfig {
20 pub listen_addr: String,
22 pub max_retries: u32,
24 pub task_timeout_secs: u64,
26 pub worker_timeout_secs: u64,
28 pub result_buffer_size: usize,
30}
31
32impl CoordinatorConfig {
33 pub fn new(listen_addr: String) -> Self {
35 Self {
36 listen_addr,
37 max_retries: 3,
38 task_timeout_secs: 300, worker_timeout_secs: 60,
40 result_buffer_size: 1000,
41 }
42 }
43
44 pub fn with_max_retries(mut self, retries: u32) -> Self {
46 self.max_retries = retries;
47 self
48 }
49
50 pub fn with_task_timeout(mut self, timeout_secs: u64) -> Self {
52 self.task_timeout_secs = timeout_secs;
53 self
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct WorkerInfo {
60 pub worker_id: String,
62 pub address: String,
64 pub status: WorkerStatus,
66 pub last_heartbeat: Instant,
68 pub active_tasks: usize,
70 pub completed_tasks: u64,
72 pub failed_tasks: u64,
74}
75
76impl WorkerInfo {
77 pub fn new(worker_id: String, address: String) -> Self {
79 Self {
80 worker_id,
81 address,
82 status: WorkerStatus::Idle,
83 last_heartbeat: Instant::now(),
84 active_tasks: 0,
85 completed_tasks: 0,
86 failed_tasks: 0,
87 }
88 }
89
90 pub fn update_heartbeat(&mut self) {
92 self.last_heartbeat = Instant::now();
93 }
94
95 pub fn is_timed_out(&self, timeout: Duration) -> bool {
97 self.last_heartbeat.elapsed() > timeout
98 }
99
100 pub fn success_rate(&self) -> f64 {
102 let total = self.completed_tasks + self.failed_tasks;
103 if total == 0 {
104 1.0
105 } else {
106 self.completed_tasks as f64 / total as f64
107 }
108 }
109}
110
111pub struct Coordinator {
113 config: CoordinatorConfig,
115 scheduler: Arc<RwLock<TaskScheduler>>,
117 workers: Arc<RwLock<HashMap<String, WorkerInfo>>>,
119 assignments: Arc<RwLock<HashMap<TaskId, String>>>,
121 results: Arc<RwLock<HashMap<TaskId, TaskResult>>>,
123 next_task_id: Arc<RwLock<u64>>,
125}
126
127impl Coordinator {
128 pub fn new(config: CoordinatorConfig) -> Self {
130 Self {
131 config,
132 scheduler: Arc::new(RwLock::new(TaskScheduler::new())),
133 workers: Arc::new(RwLock::new(HashMap::new())),
134 assignments: Arc::new(RwLock::new(HashMap::new())),
135 results: Arc::new(RwLock::new(HashMap::new())),
136 next_task_id: Arc::new(RwLock::new(0)),
137 }
138 }
139
140 pub fn add_worker(&self, worker_id: String, address: String) -> Result<()> {
142 info!("Adding worker: {} at {}", worker_id, address);
143
144 let worker_info = WorkerInfo::new(worker_id.clone(), address);
145
146 let mut workers = self
147 .workers
148 .write()
149 .map_err(|_| DistributedError::coordinator("Failed to acquire workers lock"))?;
150
151 if workers.contains_key(&worker_id) {
152 return Err(DistributedError::coordinator(format!(
153 "Worker {} already exists",
154 worker_id
155 )));
156 }
157
158 workers.insert(worker_id, worker_info);
159 Ok(())
160 }
161
162 pub fn remove_worker(&self, worker_id: &str) -> Result<()> {
164 info!("Removing worker: {}", worker_id);
165
166 let mut workers = self
167 .workers
168 .write()
169 .map_err(|_| DistributedError::coordinator("Failed to acquire workers lock"))?;
170
171 workers.remove(worker_id);
172
173 self.reassign_worker_tasks(worker_id)?;
175
176 Ok(())
177 }
178
179 pub fn update_worker_heartbeat(&self, worker_id: &str) -> Result<()> {
181 let mut workers = self
182 .workers
183 .write()
184 .map_err(|_| DistributedError::coordinator("Failed to acquire workers lock"))?;
185
186 if let Some(worker) = workers.get_mut(worker_id) {
187 worker.update_heartbeat();
188 debug!("Updated heartbeat for worker {}", worker_id);
189 Ok(())
190 } else {
191 Err(DistributedError::coordinator(format!(
192 "Worker {} not found",
193 worker_id
194 )))
195 }
196 }
197
198 pub fn check_worker_timeouts(&self) -> Result<Vec<String>> {
200 let timeout = Duration::from_secs(self.config.worker_timeout_secs);
201 let mut timed_out = Vec::new();
202
203 let workers = self
204 .workers
205 .read()
206 .map_err(|_| DistributedError::coordinator("Failed to acquire workers lock"))?;
207
208 for (worker_id, worker) in workers.iter() {
209 if worker.is_timed_out(timeout) {
210 warn!("Worker {} has timed out", worker_id);
211 timed_out.push(worker_id.clone());
212 }
213 }
214
215 drop(workers);
216
217 for worker_id in &timed_out {
219 self.reassign_worker_tasks(worker_id)?;
220 self.remove_worker(worker_id)?;
221 }
222
223 Ok(timed_out)
224 }
225
226 pub fn submit_task(
228 &self,
229 partition_id: PartitionId,
230 operation: TaskOperation,
231 ) -> Result<TaskId> {
232 let task_id = self.generate_task_id()?;
233 let mut task = Task::new(task_id, partition_id, operation);
234 task.max_retries = self.config.max_retries;
235
236 let mut scheduler = self
237 .scheduler
238 .write()
239 .map_err(|_| DistributedError::coordinator("Failed to acquire scheduler lock"))?;
240
241 scheduler.add_task(task);
242 debug!("Submitted task {}", task_id);
243
244 Ok(task_id)
245 }
246
247 pub fn next_task(&self) -> Result<Option<Task>> {
249 let mut scheduler = self
250 .scheduler
251 .write()
252 .map_err(|_| DistributedError::coordinator("Failed to acquire scheduler lock"))?;
253
254 Ok(scheduler.next_task())
255 }
256
257 pub fn assign_task(&self, task: Task, worker_id: String) -> Result<()> {
259 let mut scheduler = self
261 .scheduler
262 .write()
263 .map_err(|_| DistributedError::coordinator("Failed to acquire scheduler lock"))?;
264 scheduler.mark_running(task.clone(), worker_id.clone());
265 drop(scheduler);
266
267 let mut assignments = self
269 .assignments
270 .write()
271 .map_err(|_| DistributedError::coordinator("Failed to acquire assignments lock"))?;
272 assignments.insert(task.id, worker_id.clone());
273
274 let mut workers = self
276 .workers
277 .write()
278 .map_err(|_| DistributedError::coordinator("Failed to acquire workers lock"))?;
279 if let Some(worker) = workers.get_mut(&worker_id) {
280 worker.active_tasks += 1;
281 worker.status = WorkerStatus::Busy;
282 }
283
284 info!("Assigned task {} to worker {}", task.id, worker_id);
285 Ok(())
286 }
287
288 pub async fn dispatch_task_to_worker(
299 &self,
300 task: Task,
301 worker_id: &str,
302 input: Arc<RecordBatch>,
303 ) -> Result<TaskResult> {
304 let address = {
306 let workers = self
307 .workers
308 .read()
309 .map_err(|_| DistributedError::coordinator("Failed to acquire workers lock"))?;
310 workers
311 .get(worker_id)
312 .map(|w| w.address.clone())
313 .ok_or_else(|| {
314 DistributedError::coordinator(format!("Worker {worker_id} not found"))
315 })?
316 };
317
318 self.assign_task(task.clone(), worker_id.to_string())?;
320
321 let dispatch = async {
323 let mut client = FlightClient::new(address).await?;
324 let (response, output) = client.execute_task(&task, Some(input.as_ref())).await?;
325 Ok::<_, DistributedError>((response, output))
326 }
327 .await;
328
329 let result = match dispatch {
330 Ok((response, output)) => {
331 if response.success {
332 match output {
333 Some(batch) => TaskResult::success(
334 task.id,
335 Arc::new(batch),
336 response.execution_time_ms,
337 ),
338 None => TaskResult::failure(
339 task.id,
340 "worker reported success but returned no output batch".to_string(),
341 response.execution_time_ms,
342 ),
343 }
344 } else {
345 TaskResult::failure(
346 task.id,
347 response
348 .error
349 .unwrap_or_else(|| "worker reported failure".to_string()),
350 response.execution_time_ms,
351 )
352 }
353 }
354 Err(e) => {
357 warn!(
358 "Dispatch of task {} to {} failed: {}",
359 task.id, worker_id, e
360 );
361 TaskResult::failure(task.id, e.to_string(), 0)
362 }
363 };
364
365 self.complete_task(task.id, result.clone())?;
366 Ok(result)
367 }
368
369 pub fn complete_task(&self, task_id: TaskId, result: TaskResult) -> Result<()> {
371 let worker_id = {
372 let assignments = self
373 .assignments
374 .read()
375 .map_err(|_| DistributedError::coordinator("Failed to acquire assignments lock"))?;
376 assignments.get(&task_id).cloned()
377 };
378
379 let mut scheduler = self
381 .scheduler
382 .write()
383 .map_err(|_| DistributedError::coordinator("Failed to acquire scheduler lock"))?;
384
385 if result.is_success() {
386 scheduler.mark_completed(task_id)?;
387 } else {
388 scheduler.mark_failed(task_id)?;
389 }
390 drop(scheduler);
391
392 if let Some(worker_id) = worker_id {
394 let mut workers = self
395 .workers
396 .write()
397 .map_err(|_| DistributedError::coordinator("Failed to acquire workers lock"))?;
398
399 if let Some(worker) = workers.get_mut(&worker_id) {
400 if worker.active_tasks > 0 {
401 worker.active_tasks -= 1;
402 }
403 if result.is_success() {
404 worker.completed_tasks += 1;
405 } else {
406 worker.failed_tasks += 1;
407 }
408 if worker.active_tasks == 0 {
409 worker.status = WorkerStatus::Idle;
410 }
411 }
412 }
413
414 let mut results = self
416 .results
417 .write()
418 .map_err(|_| DistributedError::coordinator("Failed to acquire results lock"))?;
419 results.insert(task_id, result);
420
421 info!("Task {} completed", task_id);
422 Ok(())
423 }
424
425 pub fn get_available_worker(&self) -> Result<Option<String>> {
427 let workers = self
428 .workers
429 .read()
430 .map_err(|_| DistributedError::coordinator("Failed to acquire workers lock"))?;
431
432 let best_worker = workers
434 .values()
435 .filter(|w| w.status == WorkerStatus::Idle)
436 .max_by(|a, b| {
437 a.success_rate()
438 .partial_cmp(&b.success_rate())
439 .unwrap_or(std::cmp::Ordering::Equal)
440 })
441 .map(|w| w.worker_id.clone());
442
443 Ok(best_worker)
444 }
445
446 pub fn get_progress(&self) -> Result<CoordinatorProgress> {
448 let scheduler = self
449 .scheduler
450 .read()
451 .map_err(|_| DistributedError::coordinator("Failed to acquire scheduler lock"))?;
452
453 let workers = self
454 .workers
455 .read()
456 .map_err(|_| DistributedError::coordinator("Failed to acquire workers lock"))?;
457
458 Ok(CoordinatorProgress {
459 pending_tasks: scheduler.pending_count(),
460 running_tasks: scheduler.running_count(),
461 completed_tasks: scheduler.completed_count(),
462 failed_tasks: scheduler.failed_count(),
463 active_workers: workers.len(),
464 idle_workers: workers
465 .values()
466 .filter(|w| w.status == WorkerStatus::Idle)
467 .count(),
468 })
469 }
470
471 pub fn collect_results(&self) -> Result<Vec<TaskResult>> {
473 let results = self
474 .results
475 .read()
476 .map_err(|_| DistributedError::coordinator("Failed to acquire results lock"))?;
477
478 Ok(results.values().cloned().collect())
479 }
480
481 pub fn is_complete(&self) -> bool {
483 self.scheduler
484 .read()
485 .map(|s| s.is_complete())
486 .unwrap_or(false)
487 }
488
489 fn generate_task_id(&self) -> Result<TaskId> {
491 let mut next_id = self
492 .next_task_id
493 .write()
494 .map_err(|_| DistributedError::coordinator("Failed to acquire task ID lock"))?;
495 let id = *next_id;
496 *next_id += 1;
497 Ok(TaskId(id))
498 }
499
500 fn reassign_worker_tasks(&self, worker_id: &str) -> Result<()> {
502 let mut scheduler = self
503 .scheduler
504 .write()
505 .map_err(|_| DistributedError::coordinator("Failed to acquire scheduler lock"))?;
506
507 let mut assignments = self
508 .assignments
509 .write()
510 .map_err(|_| DistributedError::coordinator("Failed to acquire assignments lock"))?;
511
512 let task_ids: Vec<TaskId> = assignments
514 .iter()
515 .filter(|(_, wid)| *wid == worker_id)
516 .map(|(tid, _)| *tid)
517 .collect();
518
519 for task_id in task_ids {
521 let _ = scheduler.mark_failed(task_id);
522 assignments.remove(&task_id);
523 }
524
525 Ok(())
526 }
527
528 pub fn list_workers(&self) -> Result<Vec<WorkerInfo>> {
530 let workers = self
531 .workers
532 .read()
533 .map_err(|_| DistributedError::coordinator("Failed to acquire workers lock"))?;
534
535 Ok(workers.values().cloned().collect())
536 }
537
538 pub async fn start_monitoring(
540 self: Arc<Self>,
541 mut shutdown_rx: mpsc::Receiver<()>,
542 ) -> Result<()> {
543 info!("Starting coordinator monitoring loop");
544
545 let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(10));
546
547 loop {
548 tokio::select! {
549 _ = interval.tick() => {
550 if let Err(e) = self.check_worker_timeouts() {
551 error!("Error checking worker timeouts: {}", e);
552 }
553
554 let progress = self.get_progress().unwrap_or_default();
555 debug!("Progress: {:?}", progress);
556 }
557 _ = shutdown_rx.recv() => {
558 info!("Coordinator monitoring loop shutting down");
559 break;
560 }
561 }
562 }
563
564 Ok(())
565 }
566}
567
568#[derive(Debug, Clone, Default)]
570pub struct CoordinatorProgress {
571 pub pending_tasks: usize,
573 pub running_tasks: usize,
575 pub completed_tasks: usize,
577 pub failed_tasks: usize,
579 pub active_workers: usize,
581 pub idle_workers: usize,
583}
584
585impl CoordinatorProgress {
586 pub fn total_tasks(&self) -> usize {
588 self.pending_tasks + self.running_tasks + self.completed_tasks + self.failed_tasks
589 }
590
591 pub fn completion_percentage(&self) -> f64 {
593 let total = self.total_tasks();
594 if total == 0 {
595 0.0
596 } else {
597 (self.completed_tasks as f64 / total as f64) * 100.0
598 }
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605
606 #[test]
607 fn test_coordinator_config() {
608 let config = CoordinatorConfig::new("localhost:50051".to_string())
609 .with_max_retries(5)
610 .with_task_timeout(600);
611
612 assert_eq!(config.listen_addr, "localhost:50051");
613 assert_eq!(config.max_retries, 5);
614 assert_eq!(config.task_timeout_secs, 600);
615 }
616
617 #[test]
618 fn test_worker_info() {
619 let mut info = WorkerInfo::new("worker-1".to_string(), "localhost:50052".to_string());
620
621 info.completed_tasks = 8;
622 info.failed_tasks = 2;
623
624 assert_eq!(info.success_rate(), 0.8);
625 assert!(!info.is_timed_out(Duration::from_secs(60)));
626 }
627
628 #[test]
629 fn test_coordinator_creation() -> std::result::Result<(), Box<dyn std::error::Error>> {
630 let config = CoordinatorConfig::new("localhost:50051".to_string());
631 let coordinator = Coordinator::new(config);
632
633 let progress = coordinator.get_progress()?;
634 assert_eq!(progress.total_tasks(), 0);
635 assert_eq!(progress.active_workers, 0);
636 Ok(())
637 }
638
639 #[test]
640 fn test_add_worker() -> std::result::Result<(), Box<dyn std::error::Error>> {
641 let config = CoordinatorConfig::new("localhost:50051".to_string());
642 let coordinator = Coordinator::new(config);
643
644 coordinator.add_worker("worker-1".to_string(), "localhost:50052".to_string())?;
645
646 let workers = coordinator.list_workers()?;
647 assert_eq!(workers.len(), 1);
648 assert_eq!(workers[0].worker_id, "worker-1");
649 Ok(())
650 }
651
652 #[test]
653 fn test_submit_task() -> std::result::Result<(), Box<dyn std::error::Error>> {
654 let config = CoordinatorConfig::new("localhost:50051".to_string());
655 let coordinator = Coordinator::new(config);
656
657 let task_id = coordinator.submit_task(
658 PartitionId(0),
659 TaskOperation::Filter {
660 expression: "value > 10".to_string(),
661 },
662 )?;
663
664 assert_eq!(task_id, TaskId(0));
665
666 let progress = coordinator.get_progress()?;
667 assert_eq!(progress.pending_tasks, 1);
668 Ok(())
669 }
670
671 #[test]
672 fn test_progress() {
673 let progress = CoordinatorProgress {
674 pending_tasks: 10,
675 running_tasks: 5,
676 completed_tasks: 30,
677 failed_tasks: 5,
678 active_workers: 4,
679 idle_workers: 2,
680 };
681
682 assert_eq!(progress.total_tasks(), 50);
683 assert_eq!(progress.completion_percentage(), 60.0);
684 }
685}