[model-gateway] Optimize workflow engine with pre-computed dependency graph (#15503)

This commit is contained in:
Simo Lin
2025-12-19 15:44:57 -08:00
committed by GitHub
parent ba72e759ca
commit 0adfc42b99
3 changed files with 76 additions and 41 deletions
+34 -24
View File
@@ -5,7 +5,7 @@
//! wait for all dependencies to complete successfully.
use std::{
collections::{HashMap, HashSet},
collections::{HashMap, HashSet, VecDeque},
sync::Arc,
time::Duration,
};
@@ -132,8 +132,8 @@ impl WorkflowEngine {
}
/// Register a workflow definition
pub fn register_workflow(&self, definition: WorkflowDefinition) -> Result<(), String> {
// Validate DAG once at registration, not on every execution
pub fn register_workflow(&self, mut definition: WorkflowDefinition) -> Result<(), String> {
// Validate DAG and build dependency graph once at registration
definition.validate()?;
let id = definition.id.clone();
@@ -203,6 +203,9 @@ impl WorkflowEngine {
}
/// Execute a workflow with DAG-based parallel execution
///
/// Uses event-driven readiness: instead of scanning all steps each iteration,
/// we only check steps whose dependencies just completed.
async fn execute_workflow(
&self,
instance_id: WorkflowInstanceId,
@@ -214,6 +217,13 @@ impl WorkflowEngine {
let tracker: Arc<RwLock<StepTracker>> = Arc::new(RwLock::new(StepTracker::default()));
let (tx, mut rx) = mpsc::channel::<(StepId, StepResult)>(step_count.max(1));
// Initialize with steps that have no dependencies (O(1) lookup)
let mut pending_check: VecDeque<usize> = definition
.get_initial_step_indices()
.iter()
.copied()
.collect();
loop {
if self.state_store.is_cancelled(instance_id)? {
self.event_bus
@@ -222,32 +232,22 @@ impl WorkflowEngine {
return Ok(());
}
let (ready_step_indices, total_processed, running_count, blocked_by_failure) = {
// Find ready steps from pending_check (not all steps)
let (ready_step_indices, total_processed, running_count) = {
let t = tracker.read();
let ready: Vec<usize> = definition
.steps
.iter()
.enumerate()
.filter(|(_, step)| {
// Only check steps in pending_check, not all steps
let ready: Vec<usize> = pending_check
.drain(..)
.filter(|&idx| {
let step = &definition.steps[idx];
t.is_step_processable(&step.id)
&& t.are_dependencies_satisfied(&step.depends_on)
&& !t.has_failed_dependency(&step.depends_on)
})
.map(|(i, _)| i)
.collect();
let processed = t.total_processed();
let running = t.running.len();
// Check for blocked steps only if needed
let blocked = ready.is_empty()
&& running == 0
&& definition.steps.iter().any(|step| {
t.is_step_processable(&step.id) && t.has_failed_dependency(&step.depends_on)
});
(ready, processed, running, blocked)
(ready, t.total_processed(), t.running.len())
};
// Check if we're done
@@ -255,9 +255,11 @@ impl WorkflowEngine {
break;
}
// Handle blocked workflow
if ready_step_indices.is_empty() && running_count == 0 {
let error_message = if blocked_by_failure {
// Handle blocked workflow (no ready steps, none running, but work remains)
if ready_step_indices.is_empty() && running_count == 0 && pending_check.is_empty() {
// Check if blocked by failure
let has_failed = !tracker.read().failed.is_empty();
let error_message = if has_failed {
"Workflow failed due to step dependency failure".to_string()
} else {
"Workflow deadlocked: no steps ready and none running. This may indicate a scheduler bug.".to_string()
@@ -345,6 +347,14 @@ impl WorkflowEngine {
result = ?result,
"Step completed"
);
// Add dependents of completed step to pending_check (O(1) lookup)
// Only if the step succeeded or was skipped (not failed)
if matches!(result, StepResult::Success | StepResult::Skip) {
for &dep_idx in definition.get_dependent_indices(&completed_step_id) {
pending_check.push_back(dep_idx);
}
}
}
}
}