1use crate::{Shape, ShapeId, ValidationConfig, ValidationReport, ValidationViolation};
25use indexmap::IndexMap;
26use oxirs_core::model::Term;
27use scirs2_core::random::{rng, RngExt};
28use serde::{Deserialize, Serialize};
29use std::collections::HashMap;
30use std::sync::Arc;
31use std::time::{Duration, Instant};
32use tokio::sync::RwLock;
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct DistributedValidationConfig {
37 pub num_partitions: usize,
39 pub replication_factor: usize,
41 pub worker_timeout: Duration,
43 pub max_retries: usize,
45 pub load_balancing: LoadBalancingStrategy,
47 pub enable_caching: bool,
49 pub cache_ttl: Duration,
51 pub consistency_level: ConsistencyLevel,
53 pub enable_tracing: bool,
55}
56
57impl Default for DistributedValidationConfig {
58 fn default() -> Self {
59 Self {
60 num_partitions: 10,
61 replication_factor: 2,
62 worker_timeout: Duration::from_secs(30),
63 max_retries: 3,
64 load_balancing: LoadBalancingStrategy::RoundRobin,
65 enable_caching: true,
66 cache_ttl: Duration::from_secs(300),
67 consistency_level: ConsistencyLevel::Eventual,
68 enable_tracing: false,
69 }
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
75pub enum LoadBalancingStrategy {
76 RoundRobin,
78 LeastLoaded,
80 ConsistentHashing,
82 Random,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88pub enum ConsistencyLevel {
89 BestEffort,
91 Eventual,
93 Strong,
95}
96
97pub struct DistributedValidator {
99 config: DistributedValidationConfig,
101 workers: Arc<RwLock<Vec<WorkerInfo>>>,
103 shapes: Arc<RwLock<IndexMap<ShapeId, Shape>>>,
105 _validation_config: ValidationConfig,
107 stats: Arc<RwLock<DistributedStats>>,
109 cache: Arc<RwLock<HashMap<String, CachedResult>>>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct WorkerInfo {
116 pub worker_id: String,
118 pub address: String,
120 pub current_load: f64,
122 pub healthy: bool,
124 pub last_heartbeat: chrono::DateTime<chrono::Utc>,
126 pub validations_processed: usize,
128 pub avg_response_time_ms: u64,
130}
131
132#[derive(Debug, Clone)]
134struct CachedResult {
135 report: ValidationReport,
136 created: Instant,
137}
138
139#[derive(Debug, Clone, Default, Serialize, Deserialize)]
141pub struct DistributedStats {
142 pub total_distributed: usize,
144 pub total_partitions: usize,
146 pub failed_partitions: usize,
148 pub retried_partitions: usize,
150 pub cache_hits: usize,
152 pub cache_misses: usize,
154 pub avg_distribution_time_ms: f64,
156 pub avg_aggregation_time_ms: f64,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct ValidationPartition {
163 pub partition_id: usize,
165 pub focus_nodes: Vec<Term>,
167 pub assigned_worker: Option<String>,
169 pub retry_count: usize,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct PartitionResult {
176 pub partition_id: usize,
178 pub worker_id: String,
180 pub violations: Vec<ValidationViolation>,
182 pub processing_time: Duration,
184 pub success: bool,
186 pub error: Option<String>,
188}
189
190impl DistributedValidator {
191 pub fn new(
193 shapes: IndexMap<ShapeId, Shape>,
194 config: DistributedValidationConfig,
195 validation_config: ValidationConfig,
196 ) -> Self {
197 Self {
198 config,
199 workers: Arc::new(RwLock::new(Vec::new())),
200 shapes: Arc::new(RwLock::new(shapes)),
201 _validation_config: validation_config,
202 stats: Arc::new(RwLock::new(DistributedStats::default())),
203 cache: Arc::new(RwLock::new(HashMap::new())),
204 }
205 }
206
207 pub async fn register_worker(&self, worker: WorkerInfo) {
209 let mut workers = self.workers.write().await;
210 workers.push(worker);
211 }
212
213 pub async fn unregister_worker(&self, worker_id: &str) {
215 let mut workers = self.workers.write().await;
216 workers.retain(|w| w.worker_id != worker_id);
217 }
218
219 async fn get_healthy_workers(&self) -> Vec<WorkerInfo> {
221 let workers = self.workers.read().await;
222 workers.iter().filter(|w| w.healthy).cloned().collect()
223 }
224
225 pub async fn validate_distributed(
227 &self,
228 focus_nodes: Vec<Term>,
229 ) -> Result<ValidationReport, DistributedError> {
230 let start = Instant::now();
231
232 let cache_key = self.compute_cache_key(&focus_nodes);
234 if self.config.enable_caching {
235 if let Some(cached) = self.check_cache(&cache_key).await {
236 let mut stats = self.stats.write().await;
237 stats.cache_hits += 1;
238 return Ok(cached);
239 }
240 let mut stats = self.stats.write().await;
241 stats.cache_misses += 1;
242 }
243
244 let workers = self.get_healthy_workers().await;
246 if workers.is_empty() {
247 return Err(DistributedError::NoWorkersAvailable);
248 }
249
250 let partitions = self.partition_nodes(&focus_nodes).await;
252
253 let assigned_partitions = self.assign_partitions(partitions, &workers).await?;
255
256 let results = self.execute_distributed(&assigned_partitions).await?;
258
259 let report = self.aggregate_results(results).await?;
261
262 {
264 let mut stats = self.stats.write().await;
265 stats.total_distributed += 1;
266 let elapsed = start.elapsed().as_millis() as f64;
267 stats.avg_distribution_time_ms =
268 (stats.avg_distribution_time_ms * (stats.total_distributed - 1) as f64 + elapsed)
269 / stats.total_distributed as f64;
270 }
271
272 if self.config.enable_caching {
274 self.cache_result(&cache_key, &report).await;
275 }
276
277 Ok(report)
278 }
279
280 async fn partition_nodes(&self, focus_nodes: &[Term]) -> Vec<ValidationPartition> {
282 let num_partitions = self.config.num_partitions.min(focus_nodes.len());
283 let partition_size = (focus_nodes.len() + num_partitions - 1) / num_partitions;
284
285 let mut partitions = Vec::with_capacity(num_partitions);
286
287 for (i, chunk) in focus_nodes.chunks(partition_size).enumerate() {
288 partitions.push(ValidationPartition {
289 partition_id: i,
290 focus_nodes: chunk.to_vec(),
291 assigned_worker: None,
292 retry_count: 0,
293 });
294 }
295
296 {
298 let mut stats = self.stats.write().await;
299 stats.total_partitions += partitions.len();
300 }
301
302 partitions
303 }
304
305 async fn assign_partitions(
307 &self,
308 mut partitions: Vec<ValidationPartition>,
309 workers: &[WorkerInfo],
310 ) -> Result<Vec<ValidationPartition>, DistributedError> {
311 match self.config.load_balancing {
312 LoadBalancingStrategy::RoundRobin => {
313 for (i, partition) in partitions.iter_mut().enumerate() {
314 let worker_idx = i % workers.len();
315 partition.assigned_worker = Some(workers[worker_idx].worker_id.clone());
316 }
317 }
318 LoadBalancingStrategy::LeastLoaded => {
319 let mut sorted_workers: Vec<_> = workers.iter().collect();
321 sorted_workers.sort_by(|a, b| {
322 a.current_load
323 .partial_cmp(&b.current_load)
324 .unwrap_or(std::cmp::Ordering::Equal)
325 });
326
327 for (i, partition) in partitions.iter_mut().enumerate() {
328 let worker_idx = i % sorted_workers.len();
329 partition.assigned_worker = Some(sorted_workers[worker_idx].worker_id.clone());
330 }
331 }
332 LoadBalancingStrategy::ConsistentHashing => {
333 for partition in partitions.iter_mut() {
334 let hash = partition.partition_id;
336 let worker_idx = hash % workers.len();
337 partition.assigned_worker = Some(workers[worker_idx].worker_id.clone());
338 }
339 }
340 LoadBalancingStrategy::Random => {
341 let mut random = rng();
342 for partition in partitions.iter_mut() {
343 let worker_idx = random.random_range(0..workers.len());
344 partition.assigned_worker = Some(workers[worker_idx].worker_id.clone());
345 }
346 }
347 }
348
349 Ok(partitions)
350 }
351
352 async fn execute_distributed(
354 &self,
355 partitions: &[ValidationPartition],
356 ) -> Result<Vec<PartitionResult>, DistributedError> {
357 let mut results = Vec::with_capacity(partitions.len());
364 let shapes = self.shapes.read().await;
365
366 for partition in partitions {
367 let start = Instant::now();
368
369 let violations = self
371 .simulate_worker_validation(&partition.focus_nodes, &shapes)
372 .await;
373
374 results.push(PartitionResult {
375 partition_id: partition.partition_id,
376 worker_id: partition
377 .assigned_worker
378 .clone()
379 .unwrap_or_else(|| "local".to_string()),
380 violations,
381 processing_time: start.elapsed(),
382 success: true,
383 error: None,
384 });
385 }
386
387 Ok(results)
388 }
389
390 async fn simulate_worker_validation(
392 &self,
393 _focus_nodes: &[Term],
394 _shapes: &IndexMap<ShapeId, Shape>,
395 ) -> Vec<ValidationViolation> {
396 Vec::new()
399 }
400
401 async fn aggregate_results(
403 &self,
404 results: Vec<PartitionResult>,
405 ) -> Result<ValidationReport, DistributedError> {
406 let mut report = ValidationReport::new();
407 let mut all_violations = Vec::new();
408
409 for result in results {
410 if !result.success {
411 if let Some(error) = &result.error {
412 tracing::error!(
413 "Partition {} failed on worker {}: {}",
414 result.partition_id,
415 result.worker_id,
416 error
417 );
418
419 let mut stats = self.stats.write().await;
421 stats.failed_partitions += 1;
422 }
423 } else {
424 all_violations.extend(result.violations);
425 }
426 }
427
428 all_violations.sort_by(|a, b| a.focus_node.cmp(&b.focus_node));
430 all_violations.dedup_by(|a, b| {
431 a.focus_node == b.focus_node
432 && a.source_shape == b.source_shape
433 && a.source_constraint_component == b.source_constraint_component
434 });
435
436 for violation in all_violations {
438 report.add_violation(violation);
439 }
440
441 Ok(report)
442 }
443
444 fn compute_cache_key(&self, focus_nodes: &[Term]) -> String {
446 use std::collections::hash_map::DefaultHasher;
447 use std::hash::{Hash, Hasher};
448
449 let mut hasher = DefaultHasher::new();
450 for node in focus_nodes {
451 node.to_string().hash(&mut hasher);
452 }
453 format!("{:x}", hasher.finish())
454 }
455
456 async fn check_cache(&self, key: &str) -> Option<ValidationReport> {
458 let cache = self.cache.read().await;
459 if let Some(cached) = cache.get(key) {
460 if cached.created.elapsed() < self.config.cache_ttl {
461 return Some(cached.report.clone());
462 }
463 }
464 None
465 }
466
467 async fn cache_result(&self, key: &str, report: &ValidationReport) {
469 let mut cache = self.cache.write().await;
470 cache.insert(
471 key.to_string(),
472 CachedResult {
473 report: report.clone(),
474 created: Instant::now(),
475 },
476 );
477
478 cache.retain(|_, v| v.created.elapsed() < self.config.cache_ttl);
480 }
481
482 pub async fn get_stats(&self) -> DistributedStats {
484 self.stats.read().await.clone()
485 }
486
487 pub async fn reset_stats(&self) {
489 let mut stats = self.stats.write().await;
490 *stats = DistributedStats::default();
491 }
492
493 pub async fn update_shapes(&self, shapes: IndexMap<ShapeId, Shape>) {
495 let mut current_shapes = self.shapes.write().await;
496 *current_shapes = shapes;
497
498 let mut cache = self.cache.write().await;
500 cache.clear();
501 }
502
503 pub async fn health_check(&self) -> Vec<(String, bool)> {
505 let mut workers = self.workers.write().await;
506 let mut results = Vec::new();
507
508 for worker in workers.iter_mut() {
509 let elapsed = chrono::Utc::now().signed_duration_since(worker.last_heartbeat);
511 let healthy = elapsed.num_seconds() < 60;
512 worker.healthy = healthy;
513 results.push((worker.worker_id.clone(), healthy));
514 }
515
516 results
517 }
518}
519
520#[derive(Debug, Clone)]
522pub enum DistributedError {
523 NoWorkersAvailable,
525 AllWorkersFailed,
527 Timeout,
529 Communication(String),
531 Aggregation(String),
533}
534
535impl std::fmt::Display for DistributedError {
536 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
537 match self {
538 DistributedError::NoWorkersAvailable => write!(f, "No workers available"),
539 DistributedError::AllWorkersFailed => write!(f, "All workers failed"),
540 DistributedError::Timeout => write!(f, "Timeout waiting for results"),
541 DistributedError::Communication(msg) => write!(f, "Communication error: {}", msg),
542 DistributedError::Aggregation(msg) => write!(f, "Aggregation error: {}", msg),
543 }
544 }
545}
546
547impl std::error::Error for DistributedError {}
548
549pub struct DistributedValidatorBuilder {
551 config: DistributedValidationConfig,
552 shapes: IndexMap<ShapeId, Shape>,
553 validation_config: ValidationConfig,
554 workers: Vec<WorkerInfo>,
555}
556
557impl DistributedValidatorBuilder {
558 pub fn new() -> Self {
560 Self {
561 config: DistributedValidationConfig::default(),
562 shapes: IndexMap::new(),
563 validation_config: ValidationConfig::default(),
564 workers: Vec::new(),
565 }
566 }
567
568 pub fn num_partitions(mut self, n: usize) -> Self {
570 self.config.num_partitions = n;
571 self
572 }
573
574 pub fn load_balancing(mut self, strategy: LoadBalancingStrategy) -> Self {
576 self.config.load_balancing = strategy;
577 self
578 }
579
580 pub fn consistency_level(mut self, level: ConsistencyLevel) -> Self {
582 self.config.consistency_level = level;
583 self
584 }
585
586 pub fn worker_timeout(mut self, timeout: Duration) -> Self {
588 self.config.worker_timeout = timeout;
589 self
590 }
591
592 pub fn shapes(mut self, shapes: IndexMap<ShapeId, Shape>) -> Self {
594 self.shapes = shapes;
595 self
596 }
597
598 pub fn validation_config(mut self, config: ValidationConfig) -> Self {
600 self.validation_config = config;
601 self
602 }
603
604 pub fn add_worker(mut self, worker: WorkerInfo) -> Self {
606 self.workers.push(worker);
607 self
608 }
609
610 pub fn enable_caching(mut self, enable: bool) -> Self {
612 self.config.enable_caching = enable;
613 self
614 }
615
616 pub async fn build(self) -> DistributedValidator {
618 let validator = DistributedValidator::new(self.shapes, self.config, self.validation_config);
619
620 for worker in self.workers {
622 validator.register_worker(worker).await;
623 }
624
625 validator
626 }
627}
628
629impl Default for DistributedValidatorBuilder {
630 fn default() -> Self {
631 Self::new()
632 }
633}
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638
639 #[tokio::test]
640 async fn test_distributed_validator_creation() {
641 let shapes = IndexMap::new();
642 let config = DistributedValidationConfig::default();
643 let validation_config = ValidationConfig::default();
644
645 let _validator = DistributedValidator::new(shapes, config, validation_config);
646 }
647
648 #[tokio::test]
649 async fn test_worker_registration() {
650 let shapes = IndexMap::new();
651 let validator = DistributedValidator::new(
652 shapes,
653 DistributedValidationConfig::default(),
654 ValidationConfig::default(),
655 );
656
657 let worker = WorkerInfo {
658 worker_id: "worker-1".to_string(),
659 address: "localhost:8080".to_string(),
660 current_load: 0.0,
661 healthy: true,
662 last_heartbeat: chrono::Utc::now(),
663 validations_processed: 0,
664 avg_response_time_ms: 0,
665 };
666
667 validator.register_worker(worker).await;
668
669 let workers = validator.get_healthy_workers().await;
670 assert_eq!(workers.len(), 1);
671 assert_eq!(workers[0].worker_id, "worker-1");
672 }
673
674 #[tokio::test]
675 async fn test_partition_creation() {
676 let shapes = IndexMap::new();
677 let validator = DistributedValidator::new(
678 shapes,
679 DistributedValidationConfig {
680 num_partitions: 3,
681 ..Default::default()
682 },
683 ValidationConfig::default(),
684 );
685
686 let focus_nodes = vec![
687 Term::NamedNode(oxirs_core::model::NamedNode::new_unchecked(
688 "http://example.org/1",
689 )),
690 Term::NamedNode(oxirs_core::model::NamedNode::new_unchecked(
691 "http://example.org/2",
692 )),
693 Term::NamedNode(oxirs_core::model::NamedNode::new_unchecked(
694 "http://example.org/3",
695 )),
696 Term::NamedNode(oxirs_core::model::NamedNode::new_unchecked(
697 "http://example.org/4",
698 )),
699 Term::NamedNode(oxirs_core::model::NamedNode::new_unchecked(
700 "http://example.org/5",
701 )),
702 ];
703
704 let partitions = validator.partition_nodes(&focus_nodes).await;
705 assert_eq!(partitions.len(), 3);
706 }
707
708 #[tokio::test]
709 async fn test_round_robin_assignment() {
710 let shapes = IndexMap::new();
711 let validator = DistributedValidator::new(
712 shapes,
713 DistributedValidationConfig::default(),
714 ValidationConfig::default(),
715 );
716
717 let partitions = vec![
718 ValidationPartition {
719 partition_id: 0,
720 focus_nodes: vec![],
721 assigned_worker: None,
722 retry_count: 0,
723 },
724 ValidationPartition {
725 partition_id: 1,
726 focus_nodes: vec![],
727 assigned_worker: None,
728 retry_count: 0,
729 },
730 ];
731
732 let workers = vec![
733 WorkerInfo {
734 worker_id: "w1".to_string(),
735 address: "".to_string(),
736 current_load: 0.0,
737 healthy: true,
738 last_heartbeat: chrono::Utc::now(),
739 validations_processed: 0,
740 avg_response_time_ms: 0,
741 },
742 WorkerInfo {
743 worker_id: "w2".to_string(),
744 address: "".to_string(),
745 current_load: 0.0,
746 healthy: true,
747 last_heartbeat: chrono::Utc::now(),
748 validations_processed: 0,
749 avg_response_time_ms: 0,
750 },
751 ];
752
753 let assigned = validator
754 .assign_partitions(partitions, &workers)
755 .await
756 .expect("operation should succeed");
757
758 assert_eq!(assigned[0].assigned_worker, Some("w1".to_string()));
759 assert_eq!(assigned[1].assigned_worker, Some("w2".to_string()));
760 }
761
762 #[tokio::test]
763 async fn test_builder_pattern() {
764 let validator = DistributedValidatorBuilder::new()
765 .num_partitions(5)
766 .load_balancing(LoadBalancingStrategy::LeastLoaded)
767 .enable_caching(true)
768 .add_worker(WorkerInfo {
769 worker_id: "worker-1".to_string(),
770 address: "localhost:8080".to_string(),
771 current_load: 0.0,
772 healthy: true,
773 last_heartbeat: chrono::Utc::now(),
774 validations_processed: 0,
775 avg_response_time_ms: 0,
776 })
777 .build()
778 .await;
779
780 let workers = validator.get_healthy_workers().await;
781 assert_eq!(workers.len(), 1);
782 }
783
784 #[tokio::test]
785 async fn test_no_workers_error() {
786 let shapes = IndexMap::new();
787 let validator = DistributedValidator::new(
788 shapes,
789 DistributedValidationConfig::default(),
790 ValidationConfig::default(),
791 );
792
793 let focus_nodes = vec![Term::NamedNode(
794 oxirs_core::model::NamedNode::new_unchecked("http://example.org/1"),
795 )];
796
797 let result = validator.validate_distributed(focus_nodes).await;
798 assert!(matches!(result, Err(DistributedError::NoWorkersAvailable)));
799 }
800
801 #[tokio::test]
802 async fn test_stats_tracking() {
803 let shapes = IndexMap::new();
804 let validator = DistributedValidator::new(
805 shapes,
806 DistributedValidationConfig::default(),
807 ValidationConfig::default(),
808 );
809
810 let stats = validator.get_stats().await;
811 assert_eq!(stats.total_distributed, 0);
812 assert_eq!(stats.cache_hits, 0);
813 }
814}