Skip to content

Commit 77ab8d0

Browse files
committed
fix(runtime): init in dep order
Fixes #184 Signed-off-by: Bailey Hayes <[email protected]>
1 parent 5301182 commit 77ab8d0

File tree

1 file changed

+221
-2
lines changed

1 file changed

+221
-2
lines changed

crates/wash-runtime/src/engine/workload.rs

Lines changed: 221 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -505,12 +505,50 @@ impl ResolvedWorkload {
505505

506506
/// This function plugs a components imports with the exports of other components
507507
/// that are already loaded in the plugin system.
508+
///
509+
/// Components are processed in topological order based on their inter-component
510+
/// dependencies. This ensures that when a component imports from another component,
511+
/// the exporting component has already had its imports resolved and can be
512+
/// pre-instantiated.
508513
async fn resolve_workload_imports(
509514
&mut self,
510515
interface_map: &HashMap<String, Arc<str>>,
511516
) -> anyhow::Result<()> {
512-
let component_ids: Vec<Arc<str>> = self.components.read().await.keys().cloned().collect();
513-
for component_id in component_ids {
517+
// Build a dependency graph: for each component, track which other components it imports from
518+
let mut dependencies: HashMap<Arc<str>, HashSet<Arc<str>>> = HashMap::new();
519+
520+
{
521+
let components = self.components.read().await;
522+
for (component_id, component) in components.iter() {
523+
let mut deps = HashSet::new();
524+
let ty = component.metadata.component.component_type();
525+
for (import_name, import_item) in ty.imports(component.metadata.component.engine())
526+
{
527+
if matches!(import_item, ComponentItem::ComponentInstance(_))
528+
&& let Some(exporter_id) = interface_map.get(import_name)
529+
&& exporter_id != component_id
530+
{
531+
// This import is provided by another component in the workload
532+
deps.insert(exporter_id.clone());
533+
}
534+
}
535+
dependencies.insert(component_id.clone(), deps);
536+
}
537+
}
538+
539+
// Topologically sort components: components with no dependencies (or dependencies
540+
// already processed) come first. This ensures that when we process a component
541+
// that imports from another component, the exporter has already been resolved.
542+
let sorted_component_ids = topological_sort_components(&dependencies).context(
543+
"failed to determine component processing order - possible circular dependency",
544+
)?;
545+
546+
trace!(
547+
order = ?sorted_component_ids.iter().map(|id| id.as_ref()).collect::<Vec<_>>(),
548+
"processing components in topological order"
549+
);
550+
551+
for component_id in sorted_component_ids {
514552
// In order to have mutable access to both the workload component and components that need
515553
// to be instantiated as "plugins" during linking, we remove and re-add the component to the list.
516554
let mut workload_component = {
@@ -1408,6 +1446,84 @@ impl UnresolvedWorkload {
14081446
}
14091447
}
14101448

1449+
/// Performs a topological sort on components based on their inter-component dependencies.
1450+
///
1451+
/// This function uses Kahn's algorithm to produce an ordering where components
1452+
/// that export interfaces are processed before components that import those interfaces.
1453+
/// This ensures that when linking components, the exporting component's linker has
1454+
/// already been fully configured before it needs to be pre-instantiated.
1455+
///
1456+
/// # Arguments
1457+
/// * `dependencies` - A map from component ID to the set of component IDs it depends on
1458+
/// (i.e., components whose exports it imports)
1459+
///
1460+
/// # Returns
1461+
/// A vector of component IDs in topological order (dependencies first), or an error
1462+
/// if a circular dependency is detected.
1463+
fn topological_sort_components(
1464+
dependencies: &HashMap<Arc<str>, HashSet<Arc<str>>>,
1465+
) -> anyhow::Result<Vec<Arc<str>>> {
1466+
// Build in-degree map: count how many dependencies each component has
1467+
// (only counting dependencies on other components within this workload)
1468+
let mut in_degree: HashMap<Arc<str>, usize> = HashMap::new();
1469+
1470+
for (component_id, deps) in dependencies {
1471+
// Initialize entry for this component
1472+
in_degree.entry(component_id.clone()).or_insert(0);
1473+
1474+
// Count only dependencies that are part of this workload
1475+
let dep_count = deps
1476+
.iter()
1477+
.filter(|d| dependencies.contains_key(*d))
1478+
.count();
1479+
*in_degree.get_mut(component_id).unwrap() = dep_count;
1480+
}
1481+
1482+
// Start with components that have no dependencies (in-degree == 0)
1483+
// Sort for deterministic ordering
1484+
let mut queue: Vec<Arc<str>> = in_degree
1485+
.iter()
1486+
.filter(|&(_, degree)| *degree == 0)
1487+
.map(|(id, _)| id.clone())
1488+
.collect();
1489+
queue.sort();
1490+
1491+
let mut result = Vec::with_capacity(dependencies.len());
1492+
1493+
while let Some(component_id) = queue.pop() {
1494+
result.push(component_id.clone());
1495+
1496+
// Find components that depend on this one and decrease their in-degree
1497+
for (other_id, deps) in dependencies {
1498+
if deps.contains(&component_id)
1499+
&& let Some(degree) = in_degree.get_mut(other_id)
1500+
{
1501+
*degree = degree.saturating_sub(1);
1502+
if *degree == 0 && !result.contains(other_id) {
1503+
queue.push(other_id.clone());
1504+
// Re-sort to maintain determinism
1505+
queue.sort();
1506+
}
1507+
}
1508+
}
1509+
}
1510+
1511+
// Check for circular dependencies
1512+
if result.len() != dependencies.len() {
1513+
let unprocessed: Vec<_> = dependencies
1514+
.keys()
1515+
.filter(|id| !result.contains(id))
1516+
.map(|id| id.as_ref())
1517+
.collect();
1518+
bail!(
1519+
"circular dependency detected among components: {:?}",
1520+
unprocessed
1521+
);
1522+
}
1523+
1524+
Ok(result)
1525+
}
1526+
14111527
#[cfg(test)]
14121528
mod tests {
14131529
use super::*;
@@ -1838,4 +1954,107 @@ mod tests {
18381954
// Show the difference between includes and includes_bidirectional
18391955
assert!(!world.includes(&interface3));
18401956
}
1957+
1958+
/// Tests topological sort with a chain dependency: A -> B -> C
1959+
/// Expected order: C, B, A (or any valid topological order)
1960+
#[test]
1961+
fn test_topological_sort_chain() {
1962+
let a: Arc<str> = Arc::from("component-a");
1963+
let b: Arc<str> = Arc::from("component-b");
1964+
let c: Arc<str> = Arc::from("component-c");
1965+
1966+
// A depends on B, B depends on C
1967+
let mut dependencies: HashMap<Arc<str>, HashSet<Arc<str>>> = HashMap::new();
1968+
dependencies.insert(a.clone(), HashSet::from([b.clone()]));
1969+
dependencies.insert(b.clone(), HashSet::from([c.clone()]));
1970+
dependencies.insert(c.clone(), HashSet::new());
1971+
1972+
let result = topological_sort_components(&dependencies).unwrap();
1973+
1974+
// C should come before B, and B should come before A
1975+
let c_pos = result.iter().position(|x| x == &c).unwrap();
1976+
let b_pos = result.iter().position(|x| x == &b).unwrap();
1977+
let a_pos = result.iter().position(|x| x == &a).unwrap();
1978+
1979+
assert!(
1980+
c_pos < b_pos,
1981+
"C should be processed before B: C at {c_pos}, B at {b_pos}"
1982+
);
1983+
assert!(
1984+
b_pos < a_pos,
1985+
"B should be processed before A: B at {b_pos}, A at {a_pos}"
1986+
);
1987+
}
1988+
1989+
/// Tests topological sort with no dependencies
1990+
#[test]
1991+
fn test_topological_sort_no_dependencies() {
1992+
let a: Arc<str> = Arc::from("component-a");
1993+
let b: Arc<str> = Arc::from("component-b");
1994+
let c: Arc<str> = Arc::from("component-c");
1995+
1996+
let mut dependencies: HashMap<Arc<str>, HashSet<Arc<str>>> = HashMap::new();
1997+
dependencies.insert(a.clone(), HashSet::new());
1998+
dependencies.insert(b.clone(), HashSet::new());
1999+
dependencies.insert(c.clone(), HashSet::new());
2000+
2001+
let result = topological_sort_components(&dependencies).unwrap();
2002+
2003+
// All components should be present
2004+
assert_eq!(result.len(), 3);
2005+
assert!(result.contains(&a));
2006+
assert!(result.contains(&b));
2007+
assert!(result.contains(&c));
2008+
}
2009+
2010+
/// Tests topological sort with diamond dependency: A -> B, A -> C, B -> D, C -> D
2011+
#[test]
2012+
fn test_topological_sort_diamond() {
2013+
let a: Arc<str> = Arc::from("component-a");
2014+
let b: Arc<str> = Arc::from("component-b");
2015+
let c: Arc<str> = Arc::from("component-c");
2016+
let d: Arc<str> = Arc::from("component-d");
2017+
2018+
// A depends on B and C, both B and C depend on D
2019+
let mut dependencies: HashMap<Arc<str>, HashSet<Arc<str>>> = HashMap::new();
2020+
dependencies.insert(a.clone(), HashSet::from([b.clone(), c.clone()]));
2021+
dependencies.insert(b.clone(), HashSet::from([d.clone()]));
2022+
dependencies.insert(c.clone(), HashSet::from([d.clone()]));
2023+
dependencies.insert(d.clone(), HashSet::new());
2024+
2025+
let result = topological_sort_components(&dependencies).unwrap();
2026+
2027+
let a_pos = result.iter().position(|x| x == &a).unwrap();
2028+
let b_pos = result.iter().position(|x| x == &b).unwrap();
2029+
let c_pos = result.iter().position(|x| x == &c).unwrap();
2030+
let d_pos = result.iter().position(|x| x == &d).unwrap();
2031+
2032+
// D should come before B and C
2033+
assert!(d_pos < b_pos, "D should be processed before B");
2034+
assert!(d_pos < c_pos, "D should be processed before C");
2035+
// B and C should come before A
2036+
assert!(b_pos < a_pos, "B should be processed before A");
2037+
assert!(c_pos < a_pos, "C should be processed before A");
2038+
}
2039+
2040+
/// Tests topological sort with circular dependency detection
2041+
#[test]
2042+
fn test_topological_sort_circular_dependency() {
2043+
let a: Arc<str> = Arc::from("component-a");
2044+
let b: Arc<str> = Arc::from("component-b");
2045+
let c: Arc<str> = Arc::from("component-c");
2046+
2047+
// Circular: A -> B -> C -> A
2048+
let mut dependencies: HashMap<Arc<str>, HashSet<Arc<str>>> = HashMap::new();
2049+
dependencies.insert(a.clone(), HashSet::from([b.clone()]));
2050+
dependencies.insert(b.clone(), HashSet::from([c.clone()]));
2051+
dependencies.insert(c.clone(), HashSet::from([a.clone()]));
2052+
2053+
let result = topological_sort_components(&dependencies);
2054+
assert!(
2055+
result.is_err(),
2056+
"Should detect circular dependency: {:?}",
2057+
result
2058+
);
2059+
}
18412060
}

0 commit comments

Comments
 (0)