Skip to main content

oxirs_gql/
parallel_field_resolver.rs

1//! Parallel Field Resolution Optimization
2//!
3//! This module provides intelligent parallel resolution of independent GraphQL fields,
4//! significantly improving query performance by executing field resolvers concurrently
5//! when they don't have dependencies on each other.
6//!
7//! ## Features
8//!
9//! - **Dependency Analysis**: Automatically detects field dependencies
10//! - **Parallel Execution**: Concurrent resolution of independent fields
11//! - **Work Stealing**: Dynamic load balancing across worker threads
12//! - **Adaptive Parallelism**: Adjusts concurrency based on system load
13//! - **Smart Batching**: Groups related fields for efficient execution
14//! - **Resource Management**: Prevents thread pool exhaustion
15
16use 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/// Configuration for parallel field resolution
26#[derive(Debug, Clone)]
27pub struct ParallelResolutionConfig {
28    /// Enable parallel field resolution
29    pub enabled: bool,
30    /// Maximum number of concurrent field resolutions
31    pub max_concurrency: usize,
32    /// Minimum fields required to trigger parallelization
33    pub min_fields_for_parallel: usize,
34    /// Enable adaptive concurrency based on system load
35    pub adaptive_concurrency: bool,
36    /// Enable work stealing for load balancing
37    pub enable_work_stealing: bool,
38    /// Maximum queue depth for pending resolutions
39    pub max_queue_depth: usize,
40    /// Timeout for individual field resolutions
41    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/// Field identifier in a GraphQL query
62#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
63pub struct FieldId {
64    /// Parent type name
65    pub parent_type: String,
66    /// Field name
67    pub field_name: String,
68    /// Field alias (if any)
69    pub alias: Option<String>,
70    /// Path in the query (for nested fields)
71    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    /// Get the effective name (alias if present, otherwise field name)
95    pub fn effective_name(&self) -> &str {
96        self.alias.as_deref().unwrap_or(&self.field_name)
97    }
98}
99
100/// Dependency relationship between fields
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub enum FieldDependency {
103    /// Field depends on another field's result
104    DataDependency {
105        /// The field that must be resolved first
106        depends_on: FieldId,
107        /// Why this dependency exists
108        reason: String,
109    },
110    /// Field depends on parent context
111    ContextDependency {
112        /// Required context keys
113        required_context: Vec<String>,
114    },
115    /// No dependencies - can be resolved independently
116    Independent,
117}
118
119/// Field resolution task
120pub 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/// Result of a field resolution
129#[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/// Dependency graph for field resolution
139#[derive(Debug)]
140pub struct DependencyGraph {
141    /// Field dependencies
142    dependencies: HashMap<FieldId, Vec<FieldDependency>>,
143    /// Resolved fields
144    resolved: RwLock<HashSet<FieldId>>,
145    /// Fields currently being resolved
146    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    /// Check if a field can be resolved (all dependencies satisfied)
164    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        // Already resolved or being resolved
169        if resolved.contains(field_id) || in_progress.contains(field_id) {
170            return false;
171        }
172
173        // Check all dependencies
174        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                        // Context dependencies should be resolved at query start
184                        // For now, assume they're always available
185                    }
186                    FieldDependency::Independent => {}
187                }
188            }
189        }
190
191        true
192    }
193
194    /// Mark a field as being resolved
195    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    /// Mark a field as resolved
201    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    /// Get all fields that are ready to be resolved
210    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    /// Build dependency graph from field analysis
223    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                // Check for data dependencies based on field arguments
234                for arg_source in &metadata.argument_sources {
235                    if let Some(source_field) = arg_source.strip_prefix('$') {
236                        // Argument comes from another field
237                        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                // Check for context dependencies
252                if !metadata.required_context.is_empty() {
253                    field_deps.push(FieldDependency::ContextDependency {
254                        required_context: metadata.required_context.clone(),
255                    });
256                }
257
258                // If no dependencies found, mark as independent
259                if field_deps.is_empty() {
260                    field_deps.push(FieldDependency::Independent);
261                }
262            } else {
263                // No metadata - assume independent
264                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/// Metadata about a field for dependency analysis
281#[derive(Debug, Clone)]
282pub struct FieldMetadata {
283    /// Sources of field arguments (field names or literals)
284    pub argument_sources: Vec<String>,
285    /// Required context keys
286    pub required_context: Vec<String>,
287    /// Estimated execution cost
288    pub estimated_cost: f64,
289    /// Whether field is cacheable
290    pub cacheable: bool,
291}
292
293/// Parallel field resolver with intelligent scheduling
294pub 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    // `std::sync::RwLock`, not `tokio::sync::RwLock`: `get_metrics()` below is a
302    // plain sync fn that must be callable from within an async/Tokio context
303    // (e.g. from a `#[tokio::test]`), and only ever holds the lock for a couple
304    // of arithmetic ops (no `.await` inside the critical section anywhere it's
305    // used), so a short OS-thread block is safe and avoids
306    // `tokio::sync::RwLock::blocking_read()`'s documented panic when called
307    // from a thread that is currently driving a Tokio runtime.
308    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    /// Resolve fields in parallel according to their dependencies
333    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            // Fall back to sequential resolution
340            return self.resolve_sequential(tasks).await;
341        }
342
343        let start_time = Instant::now();
344        let total_fields = tasks.len();
345
346        // Track metrics
347        self.active_resolutions.set(total_fields as f64);
348        for _ in 0..total_fields {
349            self.total_resolutions.inc();
350        }
351
352        // Create task map
353        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        // Main resolution loop
363        while completed_fields.len() < total_fields {
364            // Get fields ready for resolution
365            let ready_fields = dependency_graph.get_ready_fields().await;
366
367            if ready_fields.is_empty() {
368                // Wait for in-progress tasks
369                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                    // Mark as resolved
374                    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                    // No ready fields and no in-progress tasks - deadlock!
381                    return Err(anyhow!(
382                        "Dependency deadlock detected: {}/{} fields completed",
383                        completed_fields.len(),
384                        total_fields
385                    ));
386                }
387                continue;
388            }
389
390            // Spawn resolution tasks for ready fields
391            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                    // Note: We don't use resolution_time_metric here due to Arc<Histogram> clone issues
404                    // Metrics are tracked in the main resolution loop instead
405
406                    join_set.spawn(async move {
407                        // Acquire semaphore permit
408                        let _permit = semaphore
409                            .acquire()
410                            .await
411                            .map_err(|e| anyhow::anyhow!("Semaphore error: {e}"))?;
412
413                        let start = Instant::now();
414
415                        // Resolve with timeout
416                        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            // Collect completed tasks
442            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        // Wait for remaining tasks
455        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        // Update timing accumulator from parallel results
461        {
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        // Calculate parallelization metrics
473        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    /// Sequential fallback resolution
489    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); // Sequential = no parallelization
524        Ok(results)
525    }
526
527    /// Get current metrics
528    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/// Metrics for parallel field resolution
549#[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        // field1 can be resolved (independent)
624        assert!(graph.can_resolve(&field1).await);
625
626        // field2 cannot be resolved yet (depends on field1)
627        assert!(!graph.can_resolve(&field2).await);
628
629        // Mark field1 as resolved
630        graph.mark_resolved(field1.clone()).await;
631
632        // Now field2 can be resolved
633        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        // field1 should be independent
665        assert!(matches!(
666            deps.get(&field1).expect("should succeed")[0],
667            FieldDependency::Independent
668        ));
669
670        // field2 should depend on field1
671        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        // Parallel execution should be faster than sequential (3 * 10ms = 30ms)
787        // Use relaxed threshold for debug builds / loaded CI systems
788        assert!(
789            elapsed < Duration::from_millis(500),
790            "Elapsed time {:?} should be less than 500ms for parallel execution",
791            elapsed
792        );
793
794        // Check parallelization rate (should be > 1.0 for parallel execution)
795        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        // Find field1 result
871        let field1_result = results
872            .iter()
873            .find(|r| r.field_id == field1)
874            .expect("should succeed");
875
876        // Find field2 and field3 results
877        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        // field1 should be resolved before field2 and field3
887        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}