ricecoder_orchestration/managers/
execution_ordering.rs1use crate::analyzers::DependencyGraph;
4use crate::error::{OrchestrationError, Result};
5use crate::models::Project;
6use std::collections::{HashMap, HashSet};
7
8#[derive(Debug, Clone)]
10pub struct ExecutionLevel {
11 pub projects: Vec<String>,
13
14 pub level: usize,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum ParallelizationStrategy {
21 Sequential,
23
24 LevelBased,
26
27 FullParallel,
29}
30
31#[derive(Debug, Clone)]
33pub struct ExecutionPlan {
34 pub levels: Vec<ExecutionLevel>,
36
37 pub total_projects: usize,
39
40 pub strategy: ParallelizationStrategy,
42
43 pub max_parallelism: usize,
45}
46
47pub struct ExecutionOrderer {
49 graph: DependencyGraph,
50}
51
52impl ExecutionOrderer {
53 pub fn new(graph: DependencyGraph) -> Self {
55 Self { graph }
56 }
57
58 pub fn determine_order(&self, projects: &[Project]) -> Result<Vec<String>> {
60 let plan = self.create_level_based_plan(projects)?;
62
63 let mut order = Vec::new();
65 for level in plan.levels {
66 order.extend(level.projects);
67 }
68
69 Ok(order)
70 }
71
72 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 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 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 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 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 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 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 for project in ¤t_level {
155 processed.insert(project.clone());
156
157 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 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 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 pub fn plan_rollback(
209 &self,
210 failed_project: &str,
211 execution_order: &[String],
212 ) -> Result<Vec<String>> {
213 let dependents = self.graph.get_dependents(failed_project);
215
216 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 let mut rollback_order = projects_to_rollback;
238 rollback_order.reverse();
239
240 Ok(rollback_order)
241 }
242
243 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 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); assert_eq!(plan.levels[1].projects.len(), 2); 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 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 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); assert_eq!(points[1].len(), 2); }
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 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 assert_eq!(rollback_order.len(), 1);
487 assert_eq!(rollback_order[0], "project-c");
488 }
489}