ricecoder_orchestration/managers/
execution_ordering.rs

1//! Execution ordering and parallelization strategies
2
3use crate::analyzers::DependencyGraph;
4use crate::error::{OrchestrationError, Result};
5use crate::models::Project;
6use std::collections::{HashMap, HashSet};
7
8/// Represents a level in the execution hierarchy
9#[derive(Debug, Clone)]
10pub struct ExecutionLevel {
11    /// Projects that can be executed at this level
12    pub projects: Vec<String>,
13
14    /// Level number (0 is first)
15    pub level: usize,
16}
17
18/// Strategy for determining parallelization points
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum ParallelizationStrategy {
21    /// Execute everything sequentially
22    Sequential,
23
24    /// Execute projects at the same level in parallel
25    LevelBased,
26
27    /// Execute all projects in parallel (no dependency ordering)
28    FullParallel,
29}
30
31/// Execution plan for a batch of projects
32#[derive(Debug, Clone)]
33pub struct ExecutionPlan {
34    /// Execution levels (each level can be parallelized)
35    pub levels: Vec<ExecutionLevel>,
36
37    /// Total number of projects
38    pub total_projects: usize,
39
40    /// Parallelization strategy used
41    pub strategy: ParallelizationStrategy,
42
43    /// Maximum parallelism possible
44    pub max_parallelism: usize,
45}
46
47/// Determines execution order and parallelization points
48pub struct ExecutionOrderer {
49    graph: DependencyGraph,
50}
51
52impl ExecutionOrderer {
53    /// Creates a new execution orderer
54    pub fn new(graph: DependencyGraph) -> Self {
55        Self { graph }
56    }
57
58    /// Determines the execution order for a set of projects
59    pub fn determine_order(&self, projects: &[Project]) -> Result<Vec<String>> {
60        // Use level-based planning to determine order
61        let plan = self.create_level_based_plan(projects)?;
62
63        // Flatten the levels into a single execution order
64        let mut order = Vec::new();
65        for level in plan.levels {
66            order.extend(level.projects);
67        }
68
69        Ok(order)
70    }
71
72    /// Creates an execution plan with parallelization points
73    pub fn create_execution_plan(
74        &self,
75        projects: &[Project],
76        strategy: ParallelizationStrategy,
77    ) -> Result<ExecutionPlan> {
78        match strategy {
79            ParallelizationStrategy::Sequential => {
80                self.create_sequential_plan(projects)
81            }
82            ParallelizationStrategy::LevelBased => {
83                self.create_level_based_plan(projects)
84            }
85            ParallelizationStrategy::FullParallel => {
86                self.create_full_parallel_plan(projects)
87            }
88        }
89    }
90
91    /// Creates a sequential execution plan
92    fn create_sequential_plan(&self, projects: &[Project]) -> Result<ExecutionPlan> {
93        let order = self.determine_order(projects)?;
94
95        let levels = vec![ExecutionLevel {
96            projects: order,
97            level: 0,
98        }];
99
100        Ok(ExecutionPlan {
101            levels,
102            total_projects: projects.len(),
103            strategy: ParallelizationStrategy::Sequential,
104            max_parallelism: 1,
105        })
106    }
107
108    /// Creates a level-based execution plan (projects at same level can run in parallel)
109    fn create_level_based_plan(&self, projects: &[Project]) -> Result<ExecutionPlan> {
110        let project_names: Vec<String> = projects.iter().map(|p| p.name.clone()).collect();
111
112        // Calculate in-degrees for each project
113        // In-degree = number of projects this project depends on
114        let mut in_degree: HashMap<String, usize> = HashMap::new();
115        for name in &project_names {
116            in_degree.insert(name.clone(), 0);
117        }
118
119        // Count how many projects each project depends on (within the batch)
120        for name in &project_names {
121            let deps = self.graph.get_dependencies(name);
122            let batch_deps = deps.iter().filter(|d| project_names.contains(d)).count();
123            in_degree.insert(name.clone(), batch_deps);
124        }
125
126        // Build levels using Kahn's algorithm
127        let mut levels = Vec::new();
128        let mut processed = HashSet::new();
129        let mut current_in_degree = in_degree.clone();
130
131        while processed.len() < project_names.len() {
132            // Find all projects with in-degree 0 (no unprocessed dependencies)
133            let current_level: Vec<String> = project_names
134                .iter()
135                .filter(|name| {
136                    !processed.contains(*name)
137                        && current_in_degree.get(*name).copied().unwrap_or(0) == 0
138                })
139                .cloned()
140                .collect();
141
142            if current_level.is_empty() {
143                return Err(OrchestrationError::CircularDependency(
144                    "Circular dependency detected during level-based planning".to_string(),
145                ));
146            }
147
148            levels.push(ExecutionLevel {
149                projects: current_level.clone(),
150                level: levels.len(),
151            });
152
153            // Mark these projects as processed and update in-degrees
154            for project in &current_level {
155                processed.insert(project.clone());
156
157                // Reduce in-degree for projects that depend on this one
158                let dependents = self.graph.get_dependents(project);
159                for dependent in dependents {
160                    if project_names.contains(&dependent) && !processed.contains(&dependent) {
161                        if let Some(degree) = current_in_degree.get_mut(&dependent) {
162                            *degree = degree.saturating_sub(1);
163                        }
164                    }
165                }
166            }
167        }
168
169        let max_parallelism = levels.iter().map(|l| l.projects.len()).max().unwrap_or(1);
170
171        Ok(ExecutionPlan {
172            levels,
173            total_projects: projects.len(),
174            strategy: ParallelizationStrategy::LevelBased,
175            max_parallelism,
176        })
177    }
178
179    /// Creates a full parallel execution plan (no dependency ordering)
180    fn create_full_parallel_plan(&self, projects: &[Project]) -> Result<ExecutionPlan> {
181        let project_names: Vec<String> = projects.iter().map(|p| p.name.clone()).collect();
182
183        let levels = vec![ExecutionLevel {
184            projects: project_names.clone(),
185            level: 0,
186        }];
187
188        Ok(ExecutionPlan {
189            levels,
190            total_projects: projects.len(),
191            strategy: ParallelizationStrategy::FullParallel,
192            max_parallelism: projects.len(),
193        })
194    }
195
196    /// Determines safe parallelization points
197    pub fn find_parallelization_points(&self, projects: &[Project]) -> Result<Vec<Vec<String>>> {
198        let plan = self.create_level_based_plan(projects)?;
199
200        Ok(plan
201            .levels
202            .into_iter()
203            .map(|level| level.projects)
204            .collect())
205    }
206
207    /// Handles execution failures and determines rollback strategy
208    pub fn plan_rollback(
209        &self,
210        failed_project: &str,
211        execution_order: &[String],
212    ) -> Result<Vec<String>> {
213        // Find all projects that depend on the failed project
214        let dependents = self.graph.get_dependents(failed_project);
215
216        // Filter to only those that were executed after the failed project
217        let failed_idx = execution_order
218            .iter()
219            .position(|p| p == failed_project)
220            .ok_or_else(|| {
221                OrchestrationError::BatchExecutionFailed(format!(
222                    "Project {} not found in execution order",
223                    failed_project
224                ))
225            })?;
226
227        let projects_to_rollback: Vec<String> = execution_order
228            .iter()
229            .enumerate()
230            .filter(|(idx, name)| {
231                *idx > failed_idx && (dependents.contains(name) || self.is_transitive_dependent(name, failed_project))
232            })
233            .map(|(_, name)| name.clone())
234            .collect();
235
236        // Reverse the order for rollback
237        let mut rollback_order = projects_to_rollback;
238        rollback_order.reverse();
239
240        Ok(rollback_order)
241    }
242
243    /// Checks if a project is transitively dependent on another
244    fn is_transitive_dependent(&self, dependent: &str, project: &str) -> bool {
245        self.graph.can_reach(dependent, project)
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use crate::models::{DependencyType, ProjectStatus};
253    use std::path::PathBuf;
254
255    fn create_test_project(name: &str) -> Project {
256        Project {
257            path: PathBuf::from(format!("/path/to/{}", name)),
258            name: name.to_string(),
259            project_type: "rust".to_string(),
260            version: "0.1.0".to_string(),
261            status: ProjectStatus::Healthy,
262        }
263    }
264
265    #[test]
266    fn test_sequential_plan() {
267        let mut graph = DependencyGraph::new(false);
268        let project_a = create_test_project("project-a");
269        let project_b = create_test_project("project-b");
270
271        graph.add_project(project_a.clone()).unwrap();
272        graph.add_project(project_b.clone()).unwrap();
273
274        graph
275            .add_dependency(crate::models::ProjectDependency {
276                from: "project-a".to_string(),
277                to: "project-b".to_string(),
278                dependency_type: DependencyType::Direct,
279                version_constraint: "^0.1.0".to_string(),
280            })
281            .unwrap();
282
283        let orderer = ExecutionOrderer::new(graph);
284        let plan = orderer
285            .create_execution_plan(
286                &[project_a, project_b],
287                ParallelizationStrategy::Sequential,
288            )
289            .unwrap();
290
291        assert_eq!(plan.levels.len(), 1);
292        assert_eq!(plan.max_parallelism, 1);
293        assert_eq!(plan.strategy, ParallelizationStrategy::Sequential);
294    }
295
296    #[test]
297    fn test_level_based_plan() {
298        let mut graph = DependencyGraph::new(false);
299        let project_a = create_test_project("project-a");
300        let project_b = create_test_project("project-b");
301        let project_c = create_test_project("project-c");
302
303        graph.add_project(project_a.clone()).unwrap();
304        graph.add_project(project_b.clone()).unwrap();
305        graph.add_project(project_c.clone()).unwrap();
306
307        // B -> A, C -> A (B and C depend on A, so A executes first)
308        graph
309            .add_dependency(crate::models::ProjectDependency {
310                from: "project-b".to_string(),
311                to: "project-a".to_string(),
312                dependency_type: DependencyType::Direct,
313                version_constraint: "^0.1.0".to_string(),
314            })
315            .unwrap();
316
317        graph
318            .add_dependency(crate::models::ProjectDependency {
319                from: "project-c".to_string(),
320                to: "project-a".to_string(),
321                dependency_type: DependencyType::Direct,
322                version_constraint: "^0.1.0".to_string(),
323            })
324            .unwrap();
325
326        let orderer = ExecutionOrderer::new(graph);
327        let plan = orderer
328            .create_execution_plan(
329                &[project_a, project_b, project_c],
330                ParallelizationStrategy::LevelBased,
331            )
332            .unwrap();
333
334        assert_eq!(plan.levels.len(), 2);
335        assert_eq!(plan.levels[0].projects.len(), 1); // A
336        assert_eq!(plan.levels[1].projects.len(), 2); // B and C
337        assert_eq!(plan.max_parallelism, 2);
338    }
339
340    #[test]
341    fn test_full_parallel_plan() {
342        let mut graph = DependencyGraph::new(false);
343        let project_a = create_test_project("project-a");
344        let project_b = create_test_project("project-b");
345
346        graph.add_project(project_a.clone()).unwrap();
347        graph.add_project(project_b.clone()).unwrap();
348
349        let orderer = ExecutionOrderer::new(graph);
350        let plan = orderer
351            .create_execution_plan(
352                &[project_a, project_b],
353                ParallelizationStrategy::FullParallel,
354            )
355            .unwrap();
356
357        assert_eq!(plan.levels.len(), 1);
358        assert_eq!(plan.levels[0].projects.len(), 2);
359        assert_eq!(plan.max_parallelism, 2);
360    }
361
362    #[test]
363    fn test_determine_order() {
364        let mut graph = DependencyGraph::new(false);
365        let project_a = create_test_project("project-a");
366        let project_b = create_test_project("project-b");
367        let project_c = create_test_project("project-c");
368
369        graph.add_project(project_a.clone()).unwrap();
370        graph.add_project(project_b.clone()).unwrap();
371        graph.add_project(project_c.clone()).unwrap();
372
373        // B -> A, C -> B (C depends on B, B depends on A)
374        graph
375            .add_dependency(crate::models::ProjectDependency {
376                from: "project-b".to_string(),
377                to: "project-a".to_string(),
378                dependency_type: DependencyType::Direct,
379                version_constraint: "^0.1.0".to_string(),
380            })
381            .unwrap();
382
383        graph
384            .add_dependency(crate::models::ProjectDependency {
385                from: "project-c".to_string(),
386                to: "project-b".to_string(),
387                dependency_type: DependencyType::Direct,
388                version_constraint: "^0.1.0".to_string(),
389            })
390            .unwrap();
391
392        let orderer = ExecutionOrderer::new(graph);
393        let order = orderer
394            .determine_order(&[project_a, project_b, project_c])
395            .unwrap();
396
397        assert_eq!(order.len(), 3);
398        let a_idx = order.iter().position(|x| x == "project-a").unwrap();
399        let b_idx = order.iter().position(|x| x == "project-b").unwrap();
400        let c_idx = order.iter().position(|x| x == "project-c").unwrap();
401
402        assert!(a_idx < b_idx);
403        assert!(b_idx < c_idx);
404    }
405
406    #[test]
407    fn test_find_parallelization_points() {
408        let mut graph = DependencyGraph::new(false);
409        let project_a = create_test_project("project-a");
410        let project_b = create_test_project("project-b");
411        let project_c = create_test_project("project-c");
412
413        graph.add_project(project_a.clone()).unwrap();
414        graph.add_project(project_b.clone()).unwrap();
415        graph.add_project(project_c.clone()).unwrap();
416
417        // B -> A, C -> A (B and C depend on A)
418        graph
419            .add_dependency(crate::models::ProjectDependency {
420                from: "project-b".to_string(),
421                to: "project-a".to_string(),
422                dependency_type: DependencyType::Direct,
423                version_constraint: "^0.1.0".to_string(),
424            })
425            .unwrap();
426
427        graph
428            .add_dependency(crate::models::ProjectDependency {
429                from: "project-c".to_string(),
430                to: "project-a".to_string(),
431                dependency_type: DependencyType::Direct,
432                version_constraint: "^0.1.0".to_string(),
433            })
434            .unwrap();
435
436        let orderer = ExecutionOrderer::new(graph);
437        let points = orderer
438            .find_parallelization_points(&[project_a, project_b, project_c])
439            .unwrap();
440
441        assert_eq!(points.len(), 2);
442        assert_eq!(points[0].len(), 1); // A
443        assert_eq!(points[1].len(), 2); // B and C
444    }
445
446    #[test]
447    fn test_plan_rollback() {
448        let mut graph = DependencyGraph::new(false);
449        let project_a = create_test_project("project-a");
450        let project_b = create_test_project("project-b");
451        let project_c = create_test_project("project-c");
452
453        graph.add_project(project_a.clone()).unwrap();
454        graph.add_project(project_b.clone()).unwrap();
455        graph.add_project(project_c.clone()).unwrap();
456
457        // B -> A, C -> B (C depends on B, B depends on A)
458        graph
459            .add_dependency(crate::models::ProjectDependency {
460                from: "project-b".to_string(),
461                to: "project-a".to_string(),
462                dependency_type: DependencyType::Direct,
463                version_constraint: "^0.1.0".to_string(),
464            })
465            .unwrap();
466
467        graph
468            .add_dependency(crate::models::ProjectDependency {
469                from: "project-c".to_string(),
470                to: "project-b".to_string(),
471                dependency_type: DependencyType::Direct,
472                version_constraint: "^0.1.0".to_string(),
473            })
474            .unwrap();
475
476        let orderer = ExecutionOrderer::new(graph);
477        let execution_order = vec![
478            "project-a".to_string(),
479            "project-b".to_string(),
480            "project-c".to_string(),
481        ];
482
483        let rollback_order = orderer.plan_rollback("project-b", &execution_order).unwrap();
484
485        // Should rollback C first (it depends on B)
486        assert_eq!(rollback_order.len(), 1);
487        assert_eq!(rollback_order[0], "project-c");
488    }
489}