Dependency Resolution
Learn how to handle complex dependencies between graphs in multi-graph compositions.
Overview
Dependency resolution in multi-graph systems involves:
- Automatic dependency detection: Analyze graph dependencies from metadata
- Topological ordering: Ensure graphs are loaded in dependency order
- Circular dependency detection: Identify and resolve circular dependencies
- Version constraints: Handle version compatibility between dependent graphs
- Interface matching: Verify compatible interfaces between graphs
- Missing dependency handling: Graceful handling of unresolved dependencies
Basic Dependency Resolution
Dependency Resolver
The core component for handling graph dependencies:
#![allow(unused)] fn main() { use reflow_network::multi_graph::{DependencyResolver, DependencyError}; let resolver = DependencyResolver::new(); // Load graphs with dependencies let graphs = vec![ graph_export_a, // depends on graph_b graph_export_b, // no dependencies graph_export_c, // depends on graph_a and graph_b ]; // Resolve dependency order let ordered_graphs = resolver.resolve_dependencies(&graphs)?; // Graphs are now ordered: [graph_b, graph_a, graph_c] for graph in &ordered_graphs { let name = graph.properties.get("name").and_then(|v| v.as_str()).unwrap_or("unnamed"); println!("Loading graph: {}", name); } }
Dependency Declaration
Declare dependencies in graph metadata:
{
"properties": {
"name": "ml_trainer",
"version": "1.2.0",
"dependencies": [
"data_processor",
"feature_engineer"
]
},
"graph_dependencies": [
{
"graph_name": "data_processor",
"namespace": "data/processing",
"version_constraint": ">=1.0.0",
"required": true,
"description": "Requires processed data for training"
},
{
"graph_name": "feature_engineer",
"namespace": "ml/features",
"version_constraint": "^2.1.0",
"required": true,
"description": "Requires feature engineering pipeline"
}
]
}
Advanced Dependency Resolution
Version Constraints
Handle version compatibility:
#![allow(unused)] fn main() { use reflow_network::multi_graph::{VersionConstraint, VersionResolver}; // Define version constraints let constraints = vec![ VersionConstraint { graph_name: "data_processor".to_string(), constraint: ">=1.0.0".to_string(), required: true, }, VersionConstraint { graph_name: "ml_core".to_string(), constraint: "^2.0.0".to_string(), // Compatible with 2.x.x required: true, }, VersionConstraint { graph_name: "analytics".to_string(), constraint: "~1.5.0".to_string(), // Compatible with 1.5.x required: false, }, ]; let version_resolver = VersionResolver::new(); let resolution_result = version_resolver.resolve_versions(&graphs, &constraints)?; if resolution_result.has_conflicts() { println!("❌ Version conflicts detected:"); for conflict in &resolution_result.conflicts { println!(" {} requires {} but {} is available", conflict.dependent, conflict.required_version, conflict.available_version); } } else { println!("✅ All version constraints satisfied"); } }
Interface Compatibility
Verify interface compatibility between dependent graphs:
#![allow(unused)] fn main() { use reflow_network::multi_graph::{InterfaceResolver, InterfaceCompatibility}; let interface_resolver = InterfaceResolver::new(); // Analyze interface compatibility let compatibility_result = interface_resolver.analyze_compatibility(&ordered_graphs)?; for incompatibility in &compatibility_result.incompatibilities { match incompatibility.severity { Severity::Error => { println!("❌ Interface incompatibility: {} → {}", incompatibility.provider, incompatibility.consumer); println!(" Expected: {}", incompatibility.expected_signature); println!(" Actual: {}", incompatibility.actual_signature); }, Severity::Warning => { println!("⚠️ Interface warning: {} → {}", incompatibility.provider, incompatibility.consumer); println!(" {}", incompatibility.description); }, } } }
Conditional Dependencies
Handle dependencies that are only required under certain conditions:
#![allow(unused)] fn main() { use reflow_network::multi_graph::{ConditionalDependency, DependencyCondition}; // Define conditional dependencies in graph metadata let conditional_deps = vec![ ConditionalDependency { graph_name: "ml_trainer".to_string(), condition: DependencyCondition::EnvironmentVariable("ENABLE_ML".to_string()), version_constraint: Some(">=2.0.0".to_string()), required: true, }, ConditionalDependency { graph_name: "analytics_dashboard".to_string(), condition: DependencyCondition::ConfigValue("features.analytics".to_string()), version_constraint: None, required: false, }, ]; // Resolve conditional dependencies let resolution_context = ResolutionContext { environment_variables: HashMap::from([ ("ENABLE_ML".to_string(), "true".to_string()), ]), config_values: HashMap::from([ ("features.analytics".to_string(), serde_json::json!(true)), ]), }; let resolved_deps = resolver.resolve_conditional_dependencies( &conditional_deps, &resolution_context )?; }
Circular Dependency Detection
Identifying Cycles
Detect and report circular dependencies:
#![allow(unused)] fn main() { use reflow_network::multi_graph::CircularDependencyDetector; let cycle_detector = CircularDependencyDetector::new(); let cycle_result = cycle_detector.detect_cycles(&graphs)?; if cycle_result.has_cycles() { println!("❌ Circular dependencies detected:"); for cycle in &cycle_result.cycles { println!(" 🔄 {}", cycle.join(" → ")); // Suggest resolution strategies let suggestions = cycle_detector.suggest_resolutions(&cycle)?; for suggestion in suggestions { println!(" 💡 {}", suggestion); } } } else { println!("✅ No circular dependencies found"); } }
Cycle Resolution Strategies
Automatic strategies for resolving circular dependencies:
#![allow(unused)] fn main() { use reflow_network::multi_graph::{CycleResolutionStrategy, DependencyBreaker}; let cycle_breaker = DependencyBreaker::new(); // Strategy 1: Optional dependency promotion let resolution1 = cycle_breaker.resolve_by_optional_promotion(&cycle)?; // Strategy 2: Interface extraction let resolution2 = cycle_breaker.resolve_by_interface_extraction(&cycle)?; // Strategy 3: Dependency inversion let resolution3 = cycle_breaker.resolve_by_dependency_inversion(&cycle)?; // Apply the best resolution strategy let best_resolution = cycle_breaker.select_best_resolution(vec![ resolution1, resolution2, resolution3 ])?; println!("🔧 Applying resolution: {}", best_resolution.description); let resolved_graphs = cycle_breaker.apply_resolution(&graphs, &best_resolution)?; }
Missing Dependency Handling
Graceful Degradation
Handle missing dependencies gracefully:
#![allow(unused)] fn main() { use reflow_network::multi_graph::{MissingDependencyHandler, DegradationStrategy}; let missing_handler = MissingDependencyHandler::new(); // Configure degradation strategies let strategies = HashMap::from([ ("optional_dependencies".to_string(), DegradationStrategy::Skip), ("required_dependencies".to_string(), DegradationStrategy::Fail), ("soft_dependencies".to_string(), DegradationStrategy::Substitute), ]); missing_handler.configure_strategies(strategies); // Handle missing dependencies let resolution_result = missing_handler.handle_missing_dependencies( &graphs, &missing_deps )?; for action in &resolution_result.actions { match action { DegradationAction::Skipped(graph_name) => { println!("⏭️ Skipped optional dependency: {}", graph_name); }, DegradationAction::Substituted(original, substitute) => { println!("🔄 Substituted {} with {}", original, substitute); }, DegradationAction::Failed(graph_name, reason) => { println!("❌ Failed to resolve required dependency: {} ({})", graph_name, reason); }, } } }
Dependency Substitution
Provide alternatives for missing dependencies:
#![allow(unused)] fn main() { use reflow_network::multi_graph::DependencySubstitution; let substitutions = vec![ DependencySubstitution { original: "premium_ml_engine".to_string(), substitute: "basic_ml_engine".to_string(), compatibility_level: CompatibilityLevel::Partial, feature_differences: vec![ "Advanced model optimization not available".to_string(), "Reduced prediction accuracy".to_string(), ], }, DependencySubstitution { original: "enterprise_analytics".to_string(), substitute: "community_analytics".to_string(), compatibility_level: CompatibilityLevel::Full, feature_differences: vec![], }, ]; missing_handler.register_substitutions(substitutions); // Apply substitutions during resolution let result = missing_handler.resolve_with_substitutions(&graphs)?; }
Dependency Analysis and Reporting
Dependency Graph Visualization
Generate dependency graphs for analysis:
#![allow(unused)] fn main() { use reflow_network::multi_graph::{DependencyAnalyzer, DependencyGraph}; let analyzer = DependencyAnalyzer::new(); // Generate dependency graph let dep_graph = analyzer.build_dependency_graph(&graphs)?; // Export to various formats dep_graph.export_to_dot("dependencies.dot")?; // Graphviz DOT dep_graph.export_to_json("dependencies.json")?; // JSON format dep_graph.export_to_mermaid("dependencies.md")?; // Mermaid diagram // Analyze graph properties let analysis = analyzer.analyze_dependency_structure(&dep_graph)?; println!("📊 Dependency Analysis:"); println!(" Graphs: {}", analysis.total_graphs); println!(" Dependencies: {}", analysis.total_dependencies); println!(" Max depth: {}", analysis.max_dependency_depth); println!(" Strongly connected components: {}", analysis.scc_count); }
Impact Analysis
Analyze the impact of dependency changes:
#![allow(unused)] fn main() { use reflow_network::multi_graph::ImpactAnalyzer; let impact_analyzer = ImpactAnalyzer::new(); // Analyze impact of changing a graph let impact = impact_analyzer.analyze_change_impact( &dep_graph, "data_processor", // Graph being changed "2.0.0" // New version )?; println!("🎯 Impact Analysis for data_processor v2.0.0:"); println!(" Directly affected graphs: {}", impact.direct_dependents.len()); println!(" Transitively affected graphs: {}", impact.transitive_dependents.len()); println!(" Breaking changes detected: {}", impact.breaking_changes.len()); for change in &impact.breaking_changes { println!(" ⚠️ {}: {}", change.affected_graph, change.description); } }
Real-World Examples
Data Processing Pipeline Dependencies
#![allow(unused)] fn main() { // Example: Complex data processing pipeline with dependencies async fn resolve_data_pipeline_dependencies() -> Result<Vec<GraphExport>, DependencyError> { let graphs = vec![ // Base data collector (no dependencies) load_graph("data/ingestion/api_collector.graph.json").await?, // Data processor (depends on collector) load_graph("data/processing/cleaner.graph.json").await?, // Feature engineer (depends on processor) load_graph("ml/features/engineer.graph.json").await?, // ML trainer (depends on feature engineer) load_graph("ml/training/trainer.graph.json").await?, // Model validator (depends on trainer) load_graph("ml/validation/validator.graph.json").await?, // Inference service (depends on trainer, but not validator) load_graph("ml/inference/predictor.graph.json").await?, // Analytics dashboard (depends on multiple components) load_graph("analytics/dashboard.graph.json").await?, ]; let resolver = DependencyResolver::new(); let ordered_graphs = resolver.resolve_dependencies(&graphs)?; // Result order: collector → cleaner → engineer → trainer → [validator, predictor] → dashboard Ok(ordered_graphs) } }
ML Pipeline with Version Constraints
#![allow(unused)] fn main() { // Example: ML pipeline with strict version requirements async fn resolve_ml_pipeline_with_versions() -> Result<Vec<GraphExport>, DependencyError> { let graphs = load_ml_graphs().await?; let version_constraints = vec![ VersionConstraint { graph_name: "tensorflow_runtime".to_string(), constraint: ">=2.8.0".to_string(), required: true, }, VersionConstraint { graph_name: "data_validator".to_string(), constraint: "^1.5.0".to_string(), required: true, }, VersionConstraint { graph_name: "model_optimizer".to_string(), constraint: "~2.1.0".to_string(), required: false, }, ]; let resolver = DependencyResolver::with_version_constraints(version_constraints); // Resolve dependencies with version checking let resolution_result = resolver.resolve_with_versions(&graphs)?; if resolution_result.has_conflicts() { // Handle version conflicts for conflict in &resolution_result.conflicts { eprintln!("Version conflict: {} requires {} but {} is available", conflict.dependent, conflict.required_version, conflict.available_version); } return Err(DependencyError::VersionConflict(resolution_result.conflicts)); } Ok(resolution_result.ordered_graphs) } }
Handling Optional Dependencies
#![allow(unused)] fn main() { // Example: System with optional features and dependencies async fn resolve_with_optional_features() -> Result<Vec<GraphExport>, DependencyError> { let base_graphs = load_core_graphs().await?; let optional_graphs = load_optional_graphs().await?; let resolver = DependencyResolver::new(); // Configure optional dependency handling let config = DependencyResolutionConfig { allow_missing_optional: true, substitute_missing: true, fail_on_missing_required: true, }; resolver.configure(config); // Define substitutions for missing optional dependencies let substitutions = vec![ DependencySubstitution { original: "premium_feature_a".to_string(), substitute: "basic_feature_a".to_string(), compatibility_level: CompatibilityLevel::Partial, feature_differences: vec![ "Advanced analytics not available".to_string(), ], }, ]; resolver.register_substitutions(substitutions); // Resolve with graceful handling of missing optional dependencies let all_graphs = [base_graphs, optional_graphs].concat(); let resolution_result = resolver.resolve_with_graceful_degradation(&all_graphs)?; // Report what was included/excluded for action in &resolution_result.degradation_actions { match action { DegradationAction::Skipped(graph) => { println!("⏭️ Skipped optional feature: {}", graph); }, DegradationAction::Substituted(original, substitute) => { println!("🔄 Using {} instead of {}", substitute, original); }, } } Ok(resolution_result.ordered_graphs) } }
Testing Dependency Resolution
Unit Testing Dependencies
Test dependency resolution logic:
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_simple_dependency_resolution() { let graph_a = create_test_graph("graph_a", vec![]); let graph_b = create_test_graph("graph_b", vec!["graph_a"]); let graph_c = create_test_graph("graph_c", vec!["graph_b"]); let graphs = vec![graph_c, graph_a, graph_b]; // Intentionally unordered let resolver = DependencyResolver::new(); let ordered = resolver.resolve_dependencies(&graphs).unwrap(); assert_eq!(get_graph_name(&ordered[0]), "graph_a"); assert_eq!(get_graph_name(&ordered[1]), "graph_b"); assert_eq!(get_graph_name(&ordered[2]), "graph_c"); } #[tokio::test] async fn test_circular_dependency_detection() { let graph_a = create_test_graph("graph_a", vec!["graph_b"]); let graph_b = create_test_graph("graph_b", vec!["graph_c"]); let graph_c = create_test_graph("graph_c", vec!["graph_a"]); let graphs = vec![graph_a, graph_b, graph_c]; let resolver = DependencyResolver::new(); let result = resolver.resolve_dependencies(&graphs); assert!(matches!(result, Err(DependencyError::CircularDependency(_)))); } #[tokio::test] async fn test_version_constraint_validation() { let graph_a = create_test_graph_with_version("graph_a", "1.0.0", vec![]); let graph_b = create_test_graph_with_version("graph_b", "2.0.0", vec![ ("graph_a", ">=1.5.0") ]); let graphs = vec![graph_a, graph_b]; let resolver = DependencyResolver::new(); let result = resolver.resolve_dependencies(&graphs); assert!(matches!(result, Err(DependencyError::VersionConflict(_)))); } } }
Integration Testing
Test complete dependency resolution workflows:
#![allow(unused)] fn main() { #[tokio::test] async fn test_complete_workspace_dependency_resolution() { let workspace_path = "test_workspace"; setup_test_workspace(workspace_path).await; let discovery = WorkspaceDiscovery::new(WorkspaceConfig { root_path: PathBuf::from(workspace_path), ..Default::default() }); let workspace = discovery.discover_workspace().await.unwrap(); let resolver = DependencyResolver::new(); let ordered_graphs = resolver.resolve_dependencies(&workspace.graphs).await.unwrap(); // Verify correct ordering verify_dependency_order(&ordered_graphs); // Verify all required dependencies are satisfied verify_all_dependencies_satisfied(&ordered_graphs); cleanup_test_workspace(workspace_path).await; } }
Best Practices
1. Explicit Dependency Declaration
Always declare dependencies explicitly in graph metadata:
{
"properties": {
"name": "my_graph",
"dependencies": ["required_graph_1", "required_graph_2"]
},
"graph_dependencies": [
{
"graph_name": "required_graph_1",
"version_constraint": ">=1.0.0",
"required": true,
"description": "Provides core data processing functionality"
}
]
}
2. Use Semantic Versioning
Follow semantic versioning for graph versions:
{
"properties": {
"version": "2.1.3" // MAJOR.MINOR.PATCH
},
"graph_dependencies": [
{
"graph_name": "data_processor",
"version_constraint": "^2.0.0" // Compatible with 2.x.x
}
]
}
3. Design for Loose Coupling
Minimize dependencies between graphs:
#![allow(unused)] fn main() { // Good: Minimal, well-defined dependencies let graph_deps = vec![ GraphDependency { graph_name: "core_processor".to_string(), required: true, // Only depends on stable core functionality }, ]; // Avoid: Tight coupling with many dependencies let graph_deps = vec![ // Too many dependencies make the graph fragile GraphDependency { graph_name: "helper1".to_string(), required: true }, GraphDependency { graph_name: "helper2".to_string(), required: true }, GraphDependency { graph_name: "helper3".to_string(), required: true }, GraphDependency { graph_name: "helper4".to_string(), required: true }, ]; }
4. Test Dependency Changes
Always test the impact of dependency changes:
#![allow(unused)] fn main() { // Before making changes, analyze impact let impact = analyzer.analyze_change_impact(&dep_graph, "my_graph", "2.0.0")?; if impact.has_breaking_changes() { println!("⚠️ Breaking changes detected - review carefully"); for change in &impact.breaking_changes { println!(" - {}", change.description); } } }
5. Document Dependencies
Document why dependencies exist and what they provide:
{
"graph_dependencies": [
{
"graph_name": "ml_core",
"version_constraint": ">=2.0.0",
"required": true,
"description": "Provides tensor operations and model training infrastructure required for neural network training"
}
]
}
Next Steps
- Workspace Discovery - Discover graphs for dependency analysis
- Graph Composition - Compose resolved graphs
- Tutorial: Multi-Graph Workspace