1use crate::error::{DistributedError, Result};
7use crate::task::{Task, TaskContext, TaskId, TaskOperation, TaskResult};
8use arrow::record_batch::RecordBatch;
9use std::collections::HashMap;
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::{Arc, RwLock};
12use std::time::Instant;
13use tokio::sync::mpsc;
14use tracing::{debug, error, info, warn};
15
16#[derive(Debug, Clone)]
18pub struct WorkerConfig {
19 pub worker_id: String,
21 pub max_concurrent_tasks: usize,
23 pub memory_limit: u64,
25 pub num_cores: usize,
27 pub heartbeat_interval_secs: u64,
29}
30
31impl WorkerConfig {
32 pub fn new(worker_id: String) -> Self {
34 let num_cores = std::thread::available_parallelism()
35 .map(|n| n.get())
36 .unwrap_or(1);
37
38 Self {
39 worker_id,
40 max_concurrent_tasks: num_cores,
41 memory_limit: 4 * 1024 * 1024 * 1024, num_cores,
43 heartbeat_interval_secs: 30,
44 }
45 }
46
47 pub fn with_max_concurrent_tasks(mut self, max: usize) -> Self {
49 self.max_concurrent_tasks = max;
50 self
51 }
52
53 pub fn with_memory_limit(mut self, limit: u64) -> Self {
55 self.memory_limit = limit;
56 self
57 }
58
59 pub fn with_num_cores(mut self, cores: usize) -> Self {
61 self.num_cores = cores;
62 self
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum WorkerStatus {
69 Idle,
71 Busy,
73 ShuttingDown,
75 Offline,
77}
78
79#[derive(Debug, Clone, Default)]
81pub struct WorkerMetrics {
82 pub tasks_executed: u64,
84 pub tasks_succeeded: u64,
86 pub tasks_failed: u64,
88 pub total_execution_time_ms: u64,
90 pub memory_usage: u64,
92 pub active_tasks: u64,
94}
95
96impl WorkerMetrics {
97 pub fn record_success(&mut self, execution_time_ms: u64) {
99 self.tasks_executed += 1;
100 self.tasks_succeeded += 1;
101 self.total_execution_time_ms += execution_time_ms;
102 }
103
104 pub fn record_failure(&mut self, execution_time_ms: u64) {
106 self.tasks_executed += 1;
107 self.tasks_failed += 1;
108 self.total_execution_time_ms += execution_time_ms;
109 }
110
111 pub fn success_rate(&self) -> f64 {
113 if self.tasks_executed == 0 {
114 0.0
115 } else {
116 self.tasks_succeeded as f64 / self.tasks_executed as f64
117 }
118 }
119
120 pub fn avg_execution_time_ms(&self) -> f64 {
122 if self.tasks_executed == 0 {
123 0.0
124 } else {
125 self.total_execution_time_ms as f64 / self.tasks_executed as f64
126 }
127 }
128}
129
130pub struct Worker {
132 config: WorkerConfig,
134 status: Arc<RwLock<WorkerStatus>>,
136 metrics: Arc<RwLock<WorkerMetrics>>,
138 running_tasks: Arc<RwLock<HashMap<TaskId, Instant>>>,
140 shutdown: Arc<AtomicBool>,
142}
143
144impl Worker {
145 pub fn new(config: WorkerConfig) -> Self {
147 Self {
148 config,
149 status: Arc::new(RwLock::new(WorkerStatus::Idle)),
150 metrics: Arc::new(RwLock::new(WorkerMetrics::default())),
151 running_tasks: Arc::new(RwLock::new(HashMap::new())),
152 shutdown: Arc::new(AtomicBool::new(false)),
153 }
154 }
155
156 pub fn worker_id(&self) -> &str {
158 &self.config.worker_id
159 }
160
161 pub fn status(&self) -> WorkerStatus {
163 self.status.read().map_or(WorkerStatus::Offline, |s| *s)
164 }
165
166 pub fn metrics(&self) -> WorkerMetrics {
168 self.metrics
169 .read()
170 .map_or_else(|_| WorkerMetrics::default(), |m| m.clone())
171 }
172
173 pub fn is_available(&self) -> bool {
175 let running_count = self.running_tasks.read().map_or(0, |r| r.len());
176 running_count < self.config.max_concurrent_tasks
177 && self.status() == WorkerStatus::Idle
178 && !self.shutdown.load(Ordering::SeqCst)
179 }
180
181 pub async fn execute_task(&self, task: Task, data: Arc<RecordBatch>) -> Result<TaskResult> {
183 if self.shutdown.load(Ordering::SeqCst) {
185 return Err(DistributedError::worker_task_failure(
186 "Worker is shutting down",
187 ));
188 }
189
190 {
192 let mut status = self.status.write().map_err(|_| {
193 DistributedError::worker_task_failure("Failed to acquire status lock")
194 })?;
195 *status = WorkerStatus::Busy;
196 }
197
198 {
200 let mut running = self.running_tasks.write().map_err(|_| {
201 DistributedError::worker_task_failure("Failed to acquire running tasks lock")
202 })?;
203 running.insert(task.id, Instant::now());
204 }
205
206 let context = TaskContext::new(task.id, self.config.worker_id.clone())
208 .with_memory_limit(self.config.memory_limit)
209 .with_num_cores(self.config.num_cores);
210
211 info!(
212 "Worker {} executing task {:?}",
213 self.config.worker_id, task.id
214 );
215
216 let start = Instant::now();
217
218 let result = self
220 .execute_operation(&task.operation, data, &context)
221 .await;
222
223 let execution_time_ms = start.elapsed().as_millis() as u64;
224
225 {
227 let mut running = self.running_tasks.write().map_err(|_| {
228 DistributedError::worker_task_failure("Failed to acquire running tasks lock")
229 })?;
230 running.remove(&task.id);
231 }
232
233 {
235 let mut metrics = self.metrics.write().map_err(|_| {
236 DistributedError::worker_task_failure("Failed to acquire metrics lock")
237 })?;
238
239 match &result {
240 Ok(batch) => {
241 metrics.record_success(execution_time_ms);
242 info!(
243 "Worker {} completed task {:?} in {}ms",
244 self.config.worker_id, task.id, execution_time_ms
245 );
246
247 let task_result =
248 TaskResult::success(task.id, batch.clone(), execution_time_ms);
249
250 if self.running_tasks.read().map_or(true, |r| r.is_empty())
252 && let Ok(mut status) = self.status.write()
253 {
254 *status = WorkerStatus::Idle;
255 }
256
257 Ok(task_result)
258 }
259 Err(e) => {
260 metrics.record_failure(execution_time_ms);
261 error!(
262 "Worker {} failed task {:?}: {}",
263 self.config.worker_id, task.id, e
264 );
265
266 let task_result =
267 TaskResult::failure(task.id, e.to_string(), execution_time_ms);
268
269 if self.running_tasks.read().map_or(true, |r| r.is_empty())
271 && let Ok(mut status) = self.status.write()
272 {
273 *status = WorkerStatus::Idle;
274 }
275
276 Ok(task_result)
277 }
278 }
279 }
280 }
281
282 async fn execute_operation(
284 &self,
285 operation: &TaskOperation,
286 data: Arc<RecordBatch>,
287 _context: &TaskContext,
288 ) -> Result<Arc<RecordBatch>> {
289 match operation {
290 TaskOperation::Filter { expression } => {
291 debug!("Applying filter: {}", expression);
292 let filtered = crate::operations::apply_filter(data.as_ref(), expression)?;
293 Ok(Arc::new(filtered))
294 }
295 TaskOperation::CalculateIndex { index_type, bands } => {
296 debug!("Calculating index: {} with bands {:?}", index_type, bands);
297 let out = crate::operations::calculate_index(data.as_ref(), index_type, bands)?;
298 Ok(Arc::new(out))
299 }
300 TaskOperation::Clip {
301 min_x,
302 min_y,
303 max_x,
304 max_y,
305 } => {
306 debug!(
307 "Clipping to bbox: [{}, {}, {}, {}]",
308 min_x, min_y, max_x, max_y
309 );
310 let clipped =
311 crate::operations::clip_bbox(data.as_ref(), *min_x, *min_y, *max_x, *max_y)?;
312 Ok(Arc::new(clipped))
313 }
314 TaskOperation::Reproject { target_epsg } => {
319 debug!(
320 "Reproject to EPSG:{} requested (not implemented)",
321 target_epsg
322 );
323 Err(DistributedError::operation_not_implemented(format!(
324 "Reproject to EPSG:{} is not yet supported by the distributed worker \
325 (requires oxigdal-proj integration)",
326 target_epsg
327 )))
328 }
329 TaskOperation::Resample {
330 width,
331 height,
332 method,
333 } => {
334 debug!(
335 "Resample to {}x{} ({}) requested (not implemented)",
336 width, height, method
337 );
338 Err(DistributedError::operation_not_implemented(format!(
339 "Resample to {}x{} using '{}' is not yet supported by the distributed \
340 worker (requires oxigdal-raster integration)",
341 width, height, method
342 )))
343 }
344 TaskOperation::Convolve {
345 kernel: _,
346 kernel_width,
347 kernel_height,
348 } => {
349 debug!(
350 "Convolve {}x{} requested (not implemented)",
351 kernel_width, kernel_height
352 );
353 Err(DistributedError::operation_not_implemented(format!(
354 "Convolution with a {}x{} kernel is not yet supported by the distributed \
355 worker (requires oxigdal-raster integration)",
356 kernel_width, kernel_height
357 )))
358 }
359 TaskOperation::Custom { name, params: _ } => {
360 debug!("Custom operation '{}' requested (no handler)", name);
361 Err(DistributedError::operation_not_implemented(format!(
362 "Custom operation '{}' has no registered handler on this worker",
363 name
364 )))
365 }
366 }
367 }
368
369 pub async fn start_heartbeat(&self, heartbeat_tx: mpsc::Sender<String>) -> Result<()> {
371 let worker_id = self.config.worker_id.clone();
372 let interval = self.config.heartbeat_interval_secs;
373 let shutdown = self.shutdown.clone();
374
375 tokio::spawn(async move {
376 let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(interval));
377
378 loop {
379 interval.tick().await;
380
381 if shutdown.load(Ordering::SeqCst) {
382 debug!("Worker {} heartbeat loop shutting down", worker_id);
383 break;
384 }
385
386 if let Err(e) = heartbeat_tx.send(worker_id.clone()).await {
387 warn!("Failed to send heartbeat for worker {}: {}", worker_id, e);
388 break;
389 }
390
391 debug!("Worker {} sent heartbeat", worker_id);
392 }
393 });
394
395 Ok(())
396 }
397
398 pub async fn shutdown(&self) -> Result<()> {
400 info!("Worker {} initiating shutdown", self.config.worker_id);
401
402 self.shutdown.store(true, Ordering::SeqCst);
403
404 {
406 let mut status = self.status.write().map_err(|_| {
407 DistributedError::worker_task_failure("Failed to acquire status lock")
408 })?;
409 *status = WorkerStatus::ShuttingDown;
410 }
411
412 let timeout = tokio::time::Duration::from_secs(30);
414 let start = Instant::now();
415
416 while start.elapsed() < timeout {
417 let running_count = self.running_tasks.read().map_or(0, |r| r.len());
418 if running_count == 0 {
419 break;
420 }
421 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
422 }
423
424 {
426 let mut status = self.status.write().map_err(|_| {
427 DistributedError::worker_task_failure("Failed to acquire status lock")
428 })?;
429 *status = WorkerStatus::Offline;
430 }
431
432 info!("Worker {} shutdown complete", self.config.worker_id);
433 Ok(())
434 }
435
436 pub fn health_check(&self) -> WorkerHealthCheck {
438 let metrics = self.metrics();
439 let status = self.status();
440 let running_count = self.running_tasks.read().map_or(0, |r| r.len());
441
442 WorkerHealthCheck {
443 worker_id: self.config.worker_id.clone(),
444 status,
445 is_healthy: status != WorkerStatus::Offline,
446 active_tasks: running_count,
447 total_tasks_executed: metrics.tasks_executed,
448 success_rate: metrics.success_rate(),
449 avg_execution_time_ms: metrics.avg_execution_time_ms(),
450 memory_usage: metrics.memory_usage,
451 }
452 }
453}
454
455#[derive(Debug, Clone)]
457pub struct WorkerHealthCheck {
458 pub worker_id: String,
460 pub status: WorkerStatus,
462 pub is_healthy: bool,
464 pub active_tasks: usize,
466 pub total_tasks_executed: u64,
468 pub success_rate: f64,
470 pub avg_execution_time_ms: f64,
472 pub memory_usage: u64,
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479 use crate::task::PartitionId;
480 use arrow::array::Int32Array;
481 use arrow::datatypes::{DataType, Field, Schema};
482
483 fn create_test_batch() -> std::result::Result<Arc<RecordBatch>, Box<dyn std::error::Error>> {
484 let schema = Arc::new(Schema::new(vec![Field::new(
485 "value",
486 DataType::Int32,
487 false,
488 )]));
489
490 let array = Int32Array::from(vec![1, 2, 3, 4, 5]);
491
492 Ok(Arc::new(RecordBatch::try_new(
493 schema,
494 vec![Arc::new(array)],
495 )?))
496 }
497
498 #[test]
499 fn test_worker_config() {
500 let config = WorkerConfig::new("worker-1".to_string())
501 .with_max_concurrent_tasks(8)
502 .with_memory_limit(8 * 1024 * 1024 * 1024);
503
504 assert_eq!(config.worker_id, "worker-1");
505 assert_eq!(config.max_concurrent_tasks, 8);
506 assert_eq!(config.memory_limit, 8 * 1024 * 1024 * 1024);
507 }
508
509 #[test]
510 fn test_worker_metrics() {
511 let mut metrics = WorkerMetrics::default();
512
513 metrics.record_success(100);
514 metrics.record_success(200);
515 metrics.record_failure(150);
516
517 assert_eq!(metrics.tasks_executed, 3);
518 assert_eq!(metrics.tasks_succeeded, 2);
519 assert_eq!(metrics.tasks_failed, 1);
520 assert_eq!(metrics.total_execution_time_ms, 450);
521 assert_eq!(metrics.success_rate(), 2.0 / 3.0);
522 assert_eq!(metrics.avg_execution_time_ms(), 150.0);
523 }
524
525 #[tokio::test]
526 async fn test_worker_creation() {
527 let config = WorkerConfig::new("worker-test".to_string());
528 let worker = Worker::new(config);
529
530 assert_eq!(worker.worker_id(), "worker-test");
531 assert_eq!(worker.status(), WorkerStatus::Idle);
532 assert!(worker.is_available());
533 }
534
535 #[tokio::test]
536 async fn test_worker_execute_task() -> std::result::Result<(), Box<dyn std::error::Error>> {
537 let config = WorkerConfig::new("worker-test".to_string());
538 let worker = Worker::new(config);
539
540 let task = Task::new(
541 TaskId(1),
542 PartitionId(0),
543 TaskOperation::Filter {
544 expression: "value > 2".to_string(),
545 },
546 );
547
548 let data = create_test_batch()?;
549 let result = worker.execute_task(task, data).await;
550
551 assert!(result.is_ok());
552 let task_result = result?;
553 assert!(task_result.is_success());
554 let out = task_result
556 .data
557 .ok_or_else(|| Box::<dyn std::error::Error>::from("success result must carry data"))?;
558 assert_eq!(out.num_rows(), 3);
559 Ok(())
560 }
561
562 #[tokio::test]
563 async fn test_worker_filter_bad_expression_fails()
564 -> std::result::Result<(), Box<dyn std::error::Error>> {
565 let worker = Worker::new(WorkerConfig::new("worker-test".to_string()));
566 let task = Task::new(
567 TaskId(2),
568 PartitionId(0),
569 TaskOperation::Filter {
570 expression: "nonexistent_column > 2".to_string(),
571 },
572 );
573 let result = worker.execute_task(task, create_test_batch()?).await?;
574 assert!(result.is_failure());
576 Ok(())
577 }
578
579 #[tokio::test]
580 async fn test_worker_calculate_index_adds_column()
581 -> std::result::Result<(), Box<dyn std::error::Error>> {
582 use arrow::array::Float64Array;
583 let schema = Arc::new(Schema::new(vec![
584 Field::new("red", DataType::Float64, false),
585 Field::new("nir", DataType::Float64, false),
586 ]));
587 let batch = Arc::new(RecordBatch::try_new(
588 schema,
589 vec![
590 Arc::new(Float64Array::from(vec![1.0, 2.0])),
591 Arc::new(Float64Array::from(vec![3.0, 6.0])),
592 ],
593 )?);
594
595 let worker = Worker::new(WorkerConfig::new("worker-test".to_string()));
596 let task = Task::new(
597 TaskId(3),
598 PartitionId(0),
599 TaskOperation::CalculateIndex {
600 index_type: "NDVI".to_string(),
601 bands: vec![0, 1],
602 },
603 );
604 let result = worker.execute_task(task, batch).await?;
605 assert!(result.is_success());
606 let out = result
607 .data
608 .ok_or_else(|| Box::<dyn std::error::Error>::from("must carry data"))?;
609 assert_eq!(out.num_columns(), 3);
610 assert_eq!(out.schema().field(2).name(), "NDVI");
611 Ok(())
612 }
613
614 #[tokio::test]
615 async fn test_worker_unimplemented_ops_fail_loudly()
616 -> std::result::Result<(), Box<dyn std::error::Error>> {
617 let worker = Worker::new(WorkerConfig::new("worker-test".to_string()));
618
619 let ops = vec![
620 TaskOperation::Reproject { target_epsg: 4326 },
621 TaskOperation::Resample {
622 width: 10,
623 height: 10,
624 method: "bilinear".to_string(),
625 },
626 TaskOperation::Convolve {
627 kernel: vec![1.0; 9],
628 kernel_width: 3,
629 kernel_height: 3,
630 },
631 TaskOperation::Custom {
632 name: "mystery".to_string(),
633 params: "{}".to_string(),
634 },
635 ];
636
637 for (i, op) in ops.into_iter().enumerate() {
638 let task = Task::new(TaskId(100 + i as u64), PartitionId(0), op);
639 let result = worker.execute_task(task, create_test_batch()?).await?;
640 assert!(result.is_failure(), "operation {} should have failed", i);
643 }
644 Ok(())
645 }
646
647 #[tokio::test]
648 async fn test_worker_health_check() {
649 let config = WorkerConfig::new("worker-test".to_string());
650 let worker = Worker::new(config);
651
652 let health = worker.health_check();
653
654 assert_eq!(health.worker_id, "worker-test");
655 assert!(health.is_healthy);
656 assert_eq!(health.active_tasks, 0);
657 assert_eq!(health.total_tasks_executed, 0);
658 }
659}