1use anyhow::{anyhow, Result};
17use scirs2_core::metrics::{Counter, Gauge, Histogram};
18use serde::{Deserialize, Serialize};
19use std::collections::{HashMap, HashSet};
20use std::sync::{Arc, RwLock as StdRwLock};
21use std::time::{Duration, Instant};
22use tokio::sync::{RwLock, Semaphore};
23use tokio::task::JoinSet;
24
25#[derive(Debug, Clone)]
27pub struct ParallelResolutionConfig {
28 pub enabled: bool,
30 pub max_concurrency: usize,
32 pub min_fields_for_parallel: usize,
34 pub adaptive_concurrency: bool,
36 pub enable_work_stealing: bool,
38 pub max_queue_depth: usize,
40 pub field_timeout: Duration,
42}
43
44impl Default for ParallelResolutionConfig {
45 fn default() -> Self {
46 Self {
47 enabled: true,
48 max_concurrency: std::thread::available_parallelism()
49 .map(|n| n.get())
50 .unwrap_or(1)
51 * 2,
52 min_fields_for_parallel: 3,
53 adaptive_concurrency: true,
54 enable_work_stealing: true,
55 max_queue_depth: 1000,
56 field_timeout: Duration::from_secs(30),
57 }
58 }
59}
60
61#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
63pub struct FieldId {
64 pub parent_type: String,
66 pub field_name: String,
68 pub alias: Option<String>,
70 pub path: Vec<String>,
72}
73
74impl FieldId {
75 pub fn new(parent_type: String, field_name: String) -> Self {
76 Self {
77 parent_type,
78 field_name,
79 alias: None,
80 path: Vec::new(),
81 }
82 }
83
84 pub fn with_alias(mut self, alias: String) -> Self {
85 self.alias = Some(alias);
86 self
87 }
88
89 pub fn with_path(mut self, path: Vec<String>) -> Self {
90 self.path = path;
91 self
92 }
93
94 pub fn effective_name(&self) -> &str {
96 self.alias.as_deref().unwrap_or(&self.field_name)
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub enum FieldDependency {
103 DataDependency {
105 depends_on: FieldId,
107 reason: String,
109 },
110 ContextDependency {
112 required_context: Vec<String>,
114 },
115 Independent,
117}
118
119pub struct FieldResolutionTask<T> {
121 pub field_id: FieldId,
122 pub resolver: Arc<dyn Fn() -> Result<T> + Send + Sync>,
123 pub dependencies: Vec<FieldDependency>,
124 pub estimated_cost: f64,
125 pub priority: i32,
126}
127
128#[derive(Debug, Clone)]
130pub struct FieldResolutionResult<T> {
131 pub field_id: FieldId,
132 pub result: Result<T, String>,
133 pub execution_time: Duration,
134 #[allow(dead_code)]
135 pub resolved_at: Instant,
136}
137
138#[derive(Debug)]
140pub struct DependencyGraph {
141 dependencies: HashMap<FieldId, Vec<FieldDependency>>,
143 resolved: RwLock<HashSet<FieldId>>,
145 in_progress: RwLock<HashSet<FieldId>>,
147}
148
149impl DependencyGraph {
150 pub fn new() -> Self {
151 Self {
152 dependencies: HashMap::new(),
153 resolved: RwLock::new(HashSet::new()),
154 in_progress: RwLock::new(HashSet::new()),
155 }
156 }
157
158 pub fn with_dependencies(mut self, deps: HashMap<FieldId, Vec<FieldDependency>>) -> Self {
159 self.dependencies = deps;
160 self
161 }
162
163 pub async fn can_resolve(&self, field_id: &FieldId) -> bool {
165 let resolved = self.resolved.read().await;
166 let in_progress = self.in_progress.read().await;
167
168 if resolved.contains(field_id) || in_progress.contains(field_id) {
170 return false;
171 }
172
173 if let Some(deps) = self.dependencies.get(field_id) {
175 for dep in deps {
176 match dep {
177 FieldDependency::DataDependency { depends_on, .. } => {
178 if !resolved.contains(depends_on) {
179 return false;
180 }
181 }
182 FieldDependency::ContextDependency { .. } => {
183 }
186 FieldDependency::Independent => {}
187 }
188 }
189 }
190
191 true
192 }
193
194 pub async fn mark_in_progress(&self, field_id: FieldId) {
196 let mut in_progress = self.in_progress.write().await;
197 in_progress.insert(field_id);
198 }
199
200 pub async fn mark_resolved(&self, field_id: FieldId) {
202 let mut resolved = self.resolved.write().await;
203 let mut in_progress = self.in_progress.write().await;
204
205 in_progress.remove(&field_id);
206 resolved.insert(field_id);
207 }
208
209 pub async fn get_ready_fields(&self) -> Vec<FieldId> {
211 let mut ready = Vec::new();
212
213 for field_id in self.dependencies.keys() {
214 if self.can_resolve(field_id).await {
215 ready.push(field_id.clone());
216 }
217 }
218
219 ready
220 }
221
222 pub fn analyze_dependencies(
224 fields: &[FieldId],
225 field_metadata: &HashMap<FieldId, FieldMetadata>,
226 ) -> HashMap<FieldId, Vec<FieldDependency>> {
227 let mut dependencies = HashMap::new();
228
229 for field in fields {
230 let mut field_deps = Vec::new();
231
232 if let Some(metadata) = field_metadata.get(field) {
233 for arg_source in &metadata.argument_sources {
235 if let Some(source_field) = arg_source.strip_prefix('$') {
236 let dep_field = fields
238 .iter()
239 .find(|f| f.field_name == source_field)
240 .cloned();
241
242 if let Some(depends_on) = dep_field {
243 field_deps.push(FieldDependency::DataDependency {
244 depends_on,
245 reason: format!("Argument dependency on field '{source_field}'"),
246 });
247 }
248 }
249 }
250
251 if !metadata.required_context.is_empty() {
253 field_deps.push(FieldDependency::ContextDependency {
254 required_context: metadata.required_context.clone(),
255 });
256 }
257
258 if field_deps.is_empty() {
260 field_deps.push(FieldDependency::Independent);
261 }
262 } else {
263 field_deps.push(FieldDependency::Independent);
265 }
266
267 dependencies.insert(field.clone(), field_deps);
268 }
269
270 dependencies
271 }
272}
273
274impl Default for DependencyGraph {
275 fn default() -> Self {
276 Self::new()
277 }
278}
279
280#[derive(Debug, Clone)]
282pub struct FieldMetadata {
283 pub argument_sources: Vec<String>,
285 pub required_context: Vec<String>,
287 pub estimated_cost: f64,
289 pub cacheable: bool,
291}
292
293pub struct ParallelFieldResolver {
295 config: ParallelResolutionConfig,
296 concurrency_semaphore: Arc<Semaphore>,
297 active_resolutions: Arc<Gauge>,
298 total_resolutions: Arc<Counter>,
299 resolution_time: Arc<Histogram>,
300 parallelization_rate: Arc<Gauge>,
301 timing_accumulator: Arc<StdRwLock<(f64, u64)>>,
309}
310
311impl ParallelFieldResolver {
312 pub fn new(config: ParallelResolutionConfig) -> Self {
313 Self {
314 concurrency_semaphore: Arc::new(Semaphore::new(config.max_concurrency)),
315 active_resolutions: Arc::new(Gauge::new(
316 "parallel_resolver_active_resolutions".to_string(),
317 )),
318 total_resolutions: Arc::new(Counter::new(
319 "parallel_resolver_total_resolutions".to_string(),
320 )),
321 resolution_time: Arc::new(Histogram::new(
322 "parallel_resolver_resolution_time_ms".to_string(),
323 )),
324 parallelization_rate: Arc::new(Gauge::new(
325 "parallel_resolver_parallelization_rate".to_string(),
326 )),
327 timing_accumulator: Arc::new(StdRwLock::new((0.0, 0))),
328 config,
329 }
330 }
331
332 pub async fn resolve_fields<T: Clone + Send + Sync + 'static>(
334 &self,
335 tasks: Vec<FieldResolutionTask<T>>,
336 dependency_graph: Arc<DependencyGraph>,
337 ) -> Result<Vec<FieldResolutionResult<T>>, anyhow::Error> {
338 if !self.config.enabled || tasks.len() < self.config.min_fields_for_parallel {
339 return self.resolve_sequential(tasks).await;
341 }
342
343 let start_time = Instant::now();
344 let total_fields = tasks.len();
345
346 self.active_resolutions.set(total_fields as f64);
348 for _ in 0..total_fields {
349 self.total_resolutions.inc();
350 }
351
352 let task_map: HashMap<FieldId, FieldResolutionTask<T>> = tasks
354 .into_iter()
355 .map(|task| (task.field_id.clone(), task))
356 .collect();
357
358 let mut results: Vec<FieldResolutionResult<T>> = Vec::new();
359 let mut join_set: JoinSet<Result<FieldResolutionResult<T>, anyhow::Error>> = JoinSet::new();
360 let mut completed_fields = HashSet::new();
361
362 while completed_fields.len() < total_fields {
364 let ready_fields = dependency_graph.get_ready_fields().await;
366
367 if ready_fields.is_empty() {
368 if let Some(result) = join_set.join_next().await {
370 let field_result =
371 result.map_err(|e| anyhow::anyhow!("Task join error: {e}"))??;
372
373 dependency_graph
375 .mark_resolved(field_result.field_id.clone())
376 .await;
377 completed_fields.insert(field_result.field_id.clone());
378 results.push(field_result);
379 } else if completed_fields.len() < total_fields {
380 return Err(anyhow!(
382 "Dependency deadlock detected: {}/{} fields completed",
383 completed_fields.len(),
384 total_fields
385 ));
386 }
387 continue;
388 }
389
390 for field_id in ready_fields {
392 if completed_fields.contains(&field_id) {
393 continue;
394 }
395
396 if let Some(task) = task_map.get(&field_id) {
397 dependency_graph.mark_in_progress(field_id.clone()).await;
398
399 let field_id = task.field_id.clone();
400 let resolver = Arc::clone(&task.resolver);
401 let semaphore = Arc::clone(&self.concurrency_semaphore);
402 let timeout = self.config.field_timeout;
403 join_set.spawn(async move {
407 let _permit = semaphore
409 .acquire()
410 .await
411 .map_err(|e| anyhow::anyhow!("Semaphore error: {e}"))?;
412
413 let start = Instant::now();
414
415 let result = tokio::time::timeout(
417 timeout,
418 tokio::task::spawn_blocking(move || resolver()),
419 )
420 .await;
421
422 let execution_time = start.elapsed();
423
424 let result = match result {
425 Ok(Ok(Ok(value))) => Ok(value),
426 Ok(Ok(Err(e))) => Err(format!("Resolver error: {e}")),
427 Ok(Err(e)) => Err(format!("Task panic: {e}")),
428 Err(_) => Err(format!("Resolution timeout after {timeout:?}")),
429 };
430
431 Ok::<FieldResolutionResult<T>, anyhow::Error>(FieldResolutionResult {
432 field_id,
433 result,
434 execution_time,
435 resolved_at: Instant::now(),
436 })
437 });
438 }
439 }
440
441 while let Some(result) = join_set.try_join_next() {
443 let field_result =
444 result.map_err(|e| anyhow::anyhow!("Task join error: {e}"))??;
445
446 dependency_graph
447 .mark_resolved(field_result.field_id.clone())
448 .await;
449 completed_fields.insert(field_result.field_id.clone());
450 results.push(field_result);
451 }
452 }
453
454 while let Some(result) = join_set.join_next().await {
456 let field_result = result.map_err(|e| anyhow::anyhow!("Task join error: {e}"))??;
457 results.push(field_result);
458 }
459
460 {
462 let mut acc = self
463 .timing_accumulator
464 .write()
465 .unwrap_or_else(|poisoned| poisoned.into_inner());
466 for r in &results {
467 acc.0 += r.execution_time.as_millis() as f64;
468 acc.1 += 1;
469 }
470 }
471
472 let total_time = start_time.elapsed();
474 let sequential_time: Duration = results.iter().map(|r| r.execution_time).sum();
475
476 let parallelization = if total_time.as_secs_f64() > 0.0 {
477 sequential_time.as_secs_f64() / total_time.as_secs_f64()
478 } else {
479 1.0
480 };
481
482 self.parallelization_rate.set(parallelization);
483 self.active_resolutions.set(0.0);
484
485 Ok(results)
486 }
487
488 async fn resolve_sequential<T: Clone + Send + Sync + 'static>(
490 &self,
491 tasks: Vec<FieldResolutionTask<T>>,
492 ) -> Result<Vec<FieldResolutionResult<T>>> {
493 let mut results = Vec::new();
494
495 for task in tasks {
496 let start = Instant::now();
497 let resolver = Arc::clone(&task.resolver);
498
499 let result = tokio::task::spawn_blocking(move || resolver())
500 .await
501 .map_err(|e| anyhow!("Task error: {e}"))?;
502
503 let execution_time = start.elapsed();
504 self.resolution_time
505 .observe(execution_time.as_millis() as f64);
506 {
507 let mut acc = self
508 .timing_accumulator
509 .write()
510 .unwrap_or_else(|poisoned| poisoned.into_inner());
511 acc.0 += execution_time.as_millis() as f64;
512 acc.1 += 1;
513 }
514
515 results.push(FieldResolutionResult {
516 field_id: task.field_id,
517 result: result.map_err(|e| e.to_string()),
518 execution_time,
519 resolved_at: Instant::now(),
520 });
521 }
522
523 self.parallelization_rate.set(1.0); Ok(results)
525 }
526
527 pub fn get_metrics(&self) -> ParallelResolverMetrics {
529 ParallelResolverMetrics {
530 active_resolutions: self.active_resolutions.get() as usize,
531 total_resolutions: self.total_resolutions.get() as usize,
532 avg_resolution_time_ms: {
533 let acc = self
534 .timing_accumulator
535 .read()
536 .unwrap_or_else(|poisoned| poisoned.into_inner());
537 if acc.1 > 0 {
538 acc.0 / acc.1 as f64
539 } else {
540 0.0
541 }
542 },
543 parallelization_rate: self.parallelization_rate.get(),
544 }
545 }
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize)]
550pub struct ParallelResolverMetrics {
551 pub active_resolutions: usize,
552 pub total_resolutions: usize,
553 pub avg_resolution_time_ms: f64,
554 pub parallelization_rate: f64,
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560
561 #[test]
562 fn test_field_id_creation() {
563 let field = FieldId::new("User".to_string(), "name".to_string());
564 assert_eq!(field.parent_type, "User");
565 assert_eq!(field.field_name, "name");
566 assert_eq!(field.effective_name(), "name");
567 }
568
569 #[test]
570 fn test_field_id_with_alias() {
571 let field = FieldId::new("User".to_string(), "firstName".to_string())
572 .with_alias("name".to_string());
573
574 assert_eq!(field.effective_name(), "name");
575 }
576
577 #[test]
578 fn test_field_id_with_path() {
579 let field = FieldId::new("User".to_string(), "name".to_string())
580 .with_path(vec!["user".to_string(), "profile".to_string()]);
581
582 assert_eq!(field.path.len(), 2);
583 }
584
585 #[tokio::test]
586 async fn test_dependency_graph_creation() {
587 let graph = DependencyGraph::new();
588 assert!(graph.dependencies.is_empty());
589 }
590
591 #[tokio::test]
592 async fn test_dependency_graph_can_resolve_independent() {
593 let field1 = FieldId::new("User".to_string(), "name".to_string());
594 let field2 = FieldId::new("User".to_string(), "email".to_string());
595
596 let mut deps = HashMap::new();
597 deps.insert(field1.clone(), vec![FieldDependency::Independent]);
598 deps.insert(field2.clone(), vec![FieldDependency::Independent]);
599
600 let graph = DependencyGraph::new().with_dependencies(deps);
601
602 assert!(graph.can_resolve(&field1).await);
603 assert!(graph.can_resolve(&field2).await);
604 }
605
606 #[tokio::test]
607 async fn test_dependency_graph_data_dependency() {
608 let field1 = FieldId::new("User".to_string(), "id".to_string());
609 let field2 = FieldId::new("Post".to_string(), "posts".to_string());
610
611 let mut deps = HashMap::new();
612 deps.insert(field1.clone(), vec![FieldDependency::Independent]);
613 deps.insert(
614 field2.clone(),
615 vec![FieldDependency::DataDependency {
616 depends_on: field1.clone(),
617 reason: "Needs user ID".to_string(),
618 }],
619 );
620
621 let graph = DependencyGraph::new().with_dependencies(deps);
622
623 assert!(graph.can_resolve(&field1).await);
625
626 assert!(!graph.can_resolve(&field2).await);
628
629 graph.mark_resolved(field1.clone()).await;
631
632 assert!(graph.can_resolve(&field2).await);
634 }
635
636 #[tokio::test]
637 async fn test_dependency_analysis() {
638 let field1 = FieldId::new("Query".to_string(), "userId".to_string());
639 let field2 = FieldId::new("Query".to_string(), "userPosts".to_string());
640
641 let mut metadata = HashMap::new();
642 metadata.insert(
643 field1.clone(),
644 FieldMetadata {
645 argument_sources: vec![],
646 required_context: vec![],
647 estimated_cost: 1.0,
648 cacheable: true,
649 },
650 );
651 metadata.insert(
652 field2.clone(),
653 FieldMetadata {
654 argument_sources: vec!["$userId".to_string()],
655 required_context: vec![],
656 estimated_cost: 5.0,
657 cacheable: false,
658 },
659 );
660
661 let fields = vec![field1.clone(), field2.clone()];
662 let deps = DependencyGraph::analyze_dependencies(&fields, &metadata);
663
664 assert!(matches!(
666 deps.get(&field1).expect("should succeed")[0],
667 FieldDependency::Independent
668 ));
669
670 assert!(matches!(
672 deps.get(&field2).expect("should succeed")[0],
673 FieldDependency::DataDependency { .. }
674 ));
675 }
676
677 #[tokio::test]
678 async fn test_parallel_resolver_creation() {
679 let config = ParallelResolutionConfig::default();
680 let resolver = ParallelFieldResolver::new(config);
681
682 let metrics = resolver.get_metrics();
683 assert_eq!(metrics.active_resolutions, 0);
684 }
685
686 #[tokio::test]
687 async fn test_sequential_resolution() {
688 let config = ParallelResolutionConfig {
689 enabled: false,
690 ..Default::default()
691 };
692 let resolver = ParallelFieldResolver::new(config);
693
694 let field1 = FieldId::new("Query".to_string(), "field1".to_string());
695 let field2 = FieldId::new("Query".to_string(), "field2".to_string());
696
697 let tasks = vec![
698 FieldResolutionTask {
699 field_id: field1.clone(),
700 resolver: Arc::new(|| Ok(42)),
701 dependencies: vec![FieldDependency::Independent],
702 estimated_cost: 1.0,
703 priority: 0,
704 },
705 FieldResolutionTask {
706 field_id: field2.clone(),
707 resolver: Arc::new(|| Ok(24)),
708 dependencies: vec![FieldDependency::Independent],
709 estimated_cost: 1.0,
710 priority: 0,
711 },
712 ];
713
714 let graph = Arc::new(DependencyGraph::new());
715 let results = resolver
716 .resolve_fields(tasks, graph)
717 .await
718 .expect("should succeed");
719
720 assert_eq!(results.len(), 2);
721 assert_eq!(results[0].result.as_ref().expect("should succeed"), &42);
722 assert_eq!(results[1].result.as_ref().expect("should succeed"), &24);
723 }
724
725 #[tokio::test]
726 async fn test_parallel_resolution_independent_fields() {
727 let config = ParallelResolutionConfig {
728 enabled: true,
729 min_fields_for_parallel: 2,
730 ..Default::default()
731 };
732 let resolver = ParallelFieldResolver::new(config);
733
734 let field1 = FieldId::new("Query".to_string(), "field1".to_string());
735 let field2 = FieldId::new("Query".to_string(), "field2".to_string());
736 let field3 = FieldId::new("Query".to_string(), "field3".to_string());
737
738 let mut deps = HashMap::new();
739 deps.insert(field1.clone(), vec![FieldDependency::Independent]);
740 deps.insert(field2.clone(), vec![FieldDependency::Independent]);
741 deps.insert(field3.clone(), vec![FieldDependency::Independent]);
742
743 let tasks = vec![
744 FieldResolutionTask {
745 field_id: field1.clone(),
746 resolver: Arc::new(|| {
747 std::thread::sleep(Duration::from_millis(10));
748 Ok(1)
749 }),
750 dependencies: vec![FieldDependency::Independent],
751 estimated_cost: 1.0,
752 priority: 0,
753 },
754 FieldResolutionTask {
755 field_id: field2.clone(),
756 resolver: Arc::new(|| {
757 std::thread::sleep(Duration::from_millis(10));
758 Ok(2)
759 }),
760 dependencies: vec![FieldDependency::Independent],
761 estimated_cost: 1.0,
762 priority: 0,
763 },
764 FieldResolutionTask {
765 field_id: field3.clone(),
766 resolver: Arc::new(|| {
767 std::thread::sleep(Duration::from_millis(10));
768 Ok(3)
769 }),
770 dependencies: vec![FieldDependency::Independent],
771 estimated_cost: 1.0,
772 priority: 0,
773 },
774 ];
775
776 let graph = Arc::new(DependencyGraph::new().with_dependencies(deps));
777 let start = Instant::now();
778 let results = resolver
779 .resolve_fields(tasks, graph)
780 .await
781 .expect("should succeed");
782 let elapsed = start.elapsed();
783
784 assert_eq!(results.len(), 3);
785
786 assert!(
789 elapsed < Duration::from_millis(500),
790 "Elapsed time {:?} should be less than 500ms for parallel execution",
791 elapsed
792 );
793
794 let metrics = resolver.get_metrics();
796 assert!(metrics.parallelization_rate > 1.0);
797 }
798
799 #[tokio::test]
800 async fn test_parallel_resolution_with_dependencies() {
801 let config = ParallelResolutionConfig {
802 enabled: true,
803 min_fields_for_parallel: 2,
804 ..Default::default()
805 };
806 let resolver = ParallelFieldResolver::new(config);
807
808 let field1 = FieldId::new("Query".to_string(), "field1".to_string());
809 let field2 = FieldId::new("Query".to_string(), "field2".to_string());
810 let field3 = FieldId::new("Query".to_string(), "field3".to_string());
811
812 let mut deps = HashMap::new();
813 deps.insert(field1.clone(), vec![FieldDependency::Independent]);
814 deps.insert(
815 field2.clone(),
816 vec![FieldDependency::DataDependency {
817 depends_on: field1.clone(),
818 reason: "Depends on field1".to_string(),
819 }],
820 );
821 deps.insert(
822 field3.clone(),
823 vec![FieldDependency::DataDependency {
824 depends_on: field1.clone(),
825 reason: "Depends on field1".to_string(),
826 }],
827 );
828
829 let tasks = vec![
830 FieldResolutionTask {
831 field_id: field1.clone(),
832 resolver: Arc::new(|| {
833 std::thread::sleep(Duration::from_millis(10));
834 Ok(1)
835 }),
836 dependencies: deps.get(&field1).expect("should succeed").clone(),
837 estimated_cost: 1.0,
838 priority: 0,
839 },
840 FieldResolutionTask {
841 field_id: field2.clone(),
842 resolver: Arc::new(|| {
843 std::thread::sleep(Duration::from_millis(10));
844 Ok(2)
845 }),
846 dependencies: deps.get(&field2).expect("should succeed").clone(),
847 estimated_cost: 1.0,
848 priority: 0,
849 },
850 FieldResolutionTask {
851 field_id: field3.clone(),
852 resolver: Arc::new(|| {
853 std::thread::sleep(Duration::from_millis(10));
854 Ok(3)
855 }),
856 dependencies: deps.get(&field3).expect("should succeed").clone(),
857 estimated_cost: 1.0,
858 priority: 0,
859 },
860 ];
861
862 let graph = Arc::new(DependencyGraph::new().with_dependencies(deps));
863 let results = resolver
864 .resolve_fields(tasks, graph)
865 .await
866 .expect("should succeed");
867
868 assert_eq!(results.len(), 3);
869
870 let field1_result = results
872 .iter()
873 .find(|r| r.field_id == field1)
874 .expect("should succeed");
875
876 let field2_result = results
878 .iter()
879 .find(|r| r.field_id == field2)
880 .expect("should succeed");
881 let field3_result = results
882 .iter()
883 .find(|r| r.field_id == field3)
884 .expect("should succeed");
885
886 assert!(field1_result.resolved_at <= field2_result.resolved_at);
888 assert!(field1_result.resolved_at <= field3_result.resolved_at);
889 }
890
891 #[tokio::test]
892 async fn test_resolution_timeout() {
893 let config = ParallelResolutionConfig {
894 enabled: true,
895 min_fields_for_parallel: 1,
896 field_timeout: Duration::from_millis(50),
897 ..Default::default()
898 };
899 let resolver = ParallelFieldResolver::new(config);
900
901 let field1 = FieldId::new("Query".to_string(), "slow_field".to_string());
902
903 let mut deps = HashMap::new();
904 deps.insert(field1.clone(), vec![FieldDependency::Independent]);
905
906 let tasks = vec![FieldResolutionTask {
907 field_id: field1.clone(),
908 resolver: Arc::new(|| {
909 std::thread::sleep(Duration::from_millis(200));
910 Ok(42)
911 }),
912 dependencies: vec![FieldDependency::Independent],
913 estimated_cost: 1.0,
914 priority: 0,
915 }];
916
917 let graph = Arc::new(DependencyGraph::new().with_dependencies(deps));
918 let results = resolver
919 .resolve_fields(tasks, graph)
920 .await
921 .expect("should succeed");
922
923 assert_eq!(results.len(), 1);
924 assert!(results[0].result.is_err());
925 assert!(results[0].result.as_ref().unwrap_err().contains("timeout"));
926 }
927
928 #[test]
929 fn test_config_defaults() {
930 let config = ParallelResolutionConfig::default();
931 assert!(config.enabled);
932 assert!(config.adaptive_concurrency);
933 assert!(config.enable_work_stealing);
934 assert!(config.min_fields_for_parallel >= 2);
935 }
936
937 #[tokio::test]
938 async fn test_avg_resolution_time_tracked() {
939 let config = ParallelResolutionConfig {
940 enabled: false,
941 ..Default::default()
942 };
943 let resolver = ParallelFieldResolver::new(config);
944
945 let field1 = FieldId::new("Query".to_string(), "f1".to_string());
946 let field2 = FieldId::new("Query".to_string(), "f2".to_string());
947 let field3 = FieldId::new("Query".to_string(), "f3".to_string());
948
949 let tasks = vec![
950 FieldResolutionTask {
951 field_id: field1,
952 resolver: Arc::new(|| {
953 std::thread::sleep(Duration::from_millis(5));
954 Ok(1)
955 }),
956 dependencies: vec![FieldDependency::Independent],
957 estimated_cost: 1.0,
958 priority: 0,
959 },
960 FieldResolutionTask {
961 field_id: field2,
962 resolver: Arc::new(|| {
963 std::thread::sleep(Duration::from_millis(5));
964 Ok(2)
965 }),
966 dependencies: vec![FieldDependency::Independent],
967 estimated_cost: 1.0,
968 priority: 0,
969 },
970 FieldResolutionTask {
971 field_id: field3,
972 resolver: Arc::new(|| {
973 std::thread::sleep(Duration::from_millis(5));
974 Ok(3)
975 }),
976 dependencies: vec![FieldDependency::Independent],
977 estimated_cost: 1.0,
978 priority: 0,
979 },
980 ];
981
982 let graph = Arc::new(DependencyGraph::new());
983 let _ = resolver
984 .resolve_fields(tasks, graph)
985 .await
986 .expect("should succeed");
987
988 let metrics = resolver.get_metrics();
989 assert!(metrics.avg_resolution_time_ms > 0.0, "avg should be > 0");
990 assert!(
991 metrics.avg_resolution_time_ms < 500.0,
992 "avg should be < 500ms"
993 );
994 }
995}