[model-gateway] improve workflow engine code quality (#16977)

This commit is contained in:
Simo Lin
2026-01-12 12:14:46 -08:00
committed by GitHub
parent ed729d22b3
commit 6e158e55b4
4 changed files with 81 additions and 14 deletions
+45 -6
View File
@@ -2,6 +2,7 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
fmt,
sync::Arc, sync::Arc,
time::Duration, time::Duration,
}; };
@@ -11,6 +12,18 @@ use super::{
types::{FailureAction, RetryPolicy, StepId, WorkflowData, WorkflowId}, types::{FailureAction, RetryPolicy, StepId, WorkflowData, WorkflowId},
}; };
/// Errors that can occur during workflow validation
#[derive(Debug, Clone, thiserror::Error)]
pub enum ValidationError {
/// A step depends on another step that doesn't exist
#[error("Step '{step}' depends on non-existent step '{dependency}'")]
MissingDependency { step: StepId, dependency: StepId },
/// A cycle was detected in the workflow DAG
#[error("Cycle detected involving step '{0}'")]
CycleDetected(StepId),
}
/// Definition of a single step within a workflow /// Definition of a single step within a workflow
pub struct StepDefinition<D: WorkflowData> { pub struct StepDefinition<D: WorkflowData> {
pub id: StepId, pub id: StepId,
@@ -22,6 +35,19 @@ pub struct StepDefinition<D: WorkflowData> {
pub depends_on: Vec<StepId>, pub depends_on: Vec<StepId>,
} }
impl<D: WorkflowData> fmt::Debug for StepDefinition<D> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StepDefinition")
.field("id", &self.id)
.field("name", &self.name)
.field("retry_policy", &self.retry_policy)
.field("timeout", &self.timeout)
.field("on_failure", &self.on_failure)
.field("depends_on", &self.depends_on)
.finish_non_exhaustive()
}
}
impl<D: WorkflowData> StepDefinition<D> { impl<D: WorkflowData> StepDefinition<D> {
pub fn new( pub fn new(
id: impl Into<String>, id: impl Into<String>,
@@ -76,6 +102,18 @@ pub struct WorkflowDefinition<D: WorkflowData> {
initial_step_indices: Vec<usize>, initial_step_indices: Vec<usize>,
} }
impl<D: WorkflowData> fmt::Debug for WorkflowDefinition<D> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WorkflowDefinition")
.field("id", &self.id)
.field("name", &self.name)
.field("steps", &self.steps)
.field("default_retry_policy", &self.default_retry_policy)
.field("default_timeout", &self.default_timeout)
.finish_non_exhaustive()
}
}
impl<D: WorkflowData> WorkflowDefinition<D> { impl<D: WorkflowData> WorkflowDefinition<D> {
pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self { pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
Self { Self {
@@ -122,7 +160,8 @@ impl<D: WorkflowData> WorkflowDefinition<D> {
/// - There's a cycle in the dependencies /// - There's a cycle in the dependencies
/// ///
/// On success, pre-computes reverse dependencies for O(1) dependent lookup. /// On success, pre-computes reverse dependencies for O(1) dependent lookup.
pub fn validate(&mut self) -> Result<(), String> { #[must_use = "validation result should be checked"]
pub fn validate(&mut self) -> Result<(), ValidationError> {
// Build HashMap for O(1) lookup instead of O(n) linear search // Build HashMap for O(1) lookup instead of O(n) linear search
let steps_map: HashMap<&StepId, &StepDefinition<D>> = let steps_map: HashMap<&StepId, &StepDefinition<D>> =
self.steps.iter().map(|s| (&s.id, s)).collect(); self.steps.iter().map(|s| (&s.id, s)).collect();
@@ -131,10 +170,10 @@ impl<D: WorkflowData> WorkflowDefinition<D> {
for step in &self.steps { for step in &self.steps {
for dep in &step.depends_on { for dep in &step.depends_on {
if !steps_map.contains_key(dep) { if !steps_map.contains_key(dep) {
return Err(format!( return Err(ValidationError::MissingDependency {
"Step '{}' depends on non-existent step '{}'", step: step.id.clone(),
step.id, dep dependency: dep.clone(),
)); });
} }
} }
} }
@@ -147,7 +186,7 @@ impl<D: WorkflowData> WorkflowDefinition<D> {
if !visited.contains(&step.id) if !visited.contains(&step.id)
&& Self::has_cycle(&step.id, &steps_map, &mut visited, &mut rec_stack) && Self::has_cycle(&step.id, &steps_map, &mut visited, &mut rec_stack)
{ {
return Err(format!("Cycle detected involving step '{}'", step.id)); return Err(ValidationError::CycleDetected(step.id.clone()));
} }
} }
+27 -5
View File
@@ -100,6 +100,23 @@ impl Backoff for LinearBackoff {
} }
} }
/// Enum-based backoff implementation to avoid heap allocation
enum BackoffImpl {
Fixed(FixedBackoff),
Exponential(backoff::ExponentialBackoff),
Linear(LinearBackoff),
}
impl BackoffImpl {
fn next_backoff(&mut self) -> Option<Duration> {
match self {
BackoffImpl::Fixed(b) => b.next_backoff(),
BackoffImpl::Exponential(b) => b.next_backoff(),
BackoffImpl::Linear(b) => b.next_backoff(),
}
}
}
/// Main workflow execution engine /// Main workflow execution engine
/// ///
/// # Type Parameters /// # Type Parameters
@@ -299,7 +316,11 @@ impl<D: WorkflowData, S: StateStore<D> + 'static> WorkflowEngine<D, S> {
} }
/// Register a workflow definition /// Register a workflow definition
pub fn register_workflow(&self, mut definition: WorkflowDefinition<D>) -> Result<(), String> { #[must_use = "registration result should be checked"]
pub fn register_workflow(
&self,
mut definition: WorkflowDefinition<D>,
) -> Result<(), super::definition::ValidationError> {
// Validate DAG and build dependency graph once at registration // Validate DAG and build dependency graph once at registration
definition.validate()?; definition.validate()?;
@@ -321,6 +342,7 @@ impl<D: WorkflowData, S: StateStore<D> + 'static> WorkflowEngine<D, S> {
/// Start a new workflow instance /// Start a new workflow instance
/// ///
/// Returns `Err(WorkflowError::ShuttingDown)` if the engine is shutting down. /// Returns `Err(WorkflowError::ShuttingDown)` if the engine is shutting down.
#[must_use = "workflow instance ID should be stored or awaited"]
pub async fn start_workflow( pub async fn start_workflow(
&self, &self,
definition_id: WorkflowId, definition_id: WorkflowId,
@@ -746,19 +768,19 @@ impl<D: WorkflowData, S: StateStore<D> + 'static> WorkflowEngine<D, S> {
} }
} }
fn create_backoff(strategy: &BackoffStrategy) -> Box<dyn Backoff + Send> { fn create_backoff(strategy: &BackoffStrategy) -> BackoffImpl {
match strategy { match strategy {
BackoffStrategy::Fixed(duration) => Box::new(FixedBackoff(*duration)), BackoffStrategy::Fixed(duration) => BackoffImpl::Fixed(FixedBackoff(*duration)),
BackoffStrategy::Exponential { base, max } => { BackoffStrategy::Exponential { base, max } => {
let backoff = ExponentialBackoffBuilder::new() let backoff = ExponentialBackoffBuilder::new()
.with_initial_interval(*base) .with_initial_interval(*base)
.with_max_interval(*max) .with_max_interval(*max)
.with_max_elapsed_time(None) .with_max_elapsed_time(None)
.build(); .build();
Box::new(backoff) BackoffImpl::Exponential(backoff)
} }
BackoffStrategy::Linear { increment, max } => { BackoffStrategy::Linear { increment, max } => {
Box::new(LinearBackoff::new(*increment, *max)) BackoffImpl::Linear(LinearBackoff::new(*increment, *max))
} }
} }
} }
+1 -1
View File
@@ -7,7 +7,7 @@ mod executor;
mod state; mod state;
pub mod types; pub mod types;
pub use definition::{StepDefinition, WorkflowDefinition}; pub use definition::{StepDefinition, ValidationError, WorkflowDefinition};
pub use engine::WorkflowEngine; pub use engine::WorkflowEngine;
pub use event::{EventBus, EventSubscriber, LoggingSubscriber, WorkflowEvent}; pub use event::{EventBus, EventSubscriber, LoggingSubscriber, WorkflowEvent};
pub use executor::{FunctionStep, StepExecutor}; pub use executor::{FunctionStep, StepExecutor};
+8 -2
View File
@@ -649,7 +649,10 @@ fn test_dag_validation_cycle_detection() {
let result = workflow.validate(); let result = workflow.validate();
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().contains("Cycle detected")); assert!(matches!(
result.unwrap_err(),
ValidationError::CycleDetected(_)
));
} }
#[test] #[test]
@@ -668,7 +671,10 @@ fn test_dag_validation_missing_dependency() {
let result = workflow.validate(); let result = workflow.validate();
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().contains("non-existent step")); assert!(matches!(
result.unwrap_err(),
ValidationError::MissingDependency { .. }
));
} }
#[test] #[test]