Files
sglang/sgl-model-gateway/src/workflow/definition.rs
T

220 lines
6.7 KiB
Rust

//! Workflow definition types
use std::{
collections::{HashMap, HashSet},
sync::Arc,
time::Duration,
};
use super::{
executor::StepExecutor,
types::{FailureAction, RetryPolicy, StepId, WorkflowId},
};
/// Definition of a single step within a workflow
pub struct StepDefinition {
pub id: StepId,
pub name: String,
pub executor: Arc<dyn StepExecutor>,
pub retry_policy: Option<RetryPolicy>,
pub timeout: Option<Duration>,
pub on_failure: FailureAction,
pub depends_on: Vec<StepId>,
}
impl StepDefinition {
pub fn new(
id: impl Into<String>,
name: impl Into<String>,
executor: Arc<dyn StepExecutor>,
) -> Self {
Self {
id: StepId::new(id.into()),
name: name.into(),
executor,
retry_policy: None,
timeout: None,
on_failure: FailureAction::FailWorkflow,
depends_on: Vec::new(),
}
}
pub fn with_retry(mut self, policy: RetryPolicy) -> Self {
self.retry_policy = Some(policy);
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn with_failure_action(mut self, action: FailureAction) -> Self {
self.on_failure = action;
self
}
/// Set dependencies for this step.
/// The step will only run after all specified dependencies have completed successfully.
/// Empty slice means no dependencies - step can run immediately in parallel with others.
pub fn depends_on(mut self, deps: &[&str]) -> Self {
self.depends_on = deps.iter().map(|s| StepId::new(*s)).collect();
self
}
}
/// Complete workflow definition
pub struct WorkflowDefinition {
pub id: WorkflowId,
pub name: String,
pub steps: Vec<StepDefinition>,
pub default_retry_policy: RetryPolicy,
pub default_timeout: Duration,
/// Pre-computed reverse dependencies: step_id -> indices of steps that depend on it
reverse_deps: HashMap<StepId, Vec<usize>>,
/// Pre-computed indices of steps with no dependencies (can start immediately)
initial_step_indices: Vec<usize>,
}
impl WorkflowDefinition {
pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
Self {
id: WorkflowId::new(id.into()),
name: name.into(),
steps: Vec::new(),
default_retry_policy: RetryPolicy::default(),
default_timeout: Duration::from_secs(300), // 5 minutes
reverse_deps: HashMap::new(),
initial_step_indices: Vec::new(),
}
}
pub fn add_step(mut self, step: StepDefinition) -> Self {
self.steps.push(step);
self
}
pub fn with_default_retry(mut self, policy: RetryPolicy) -> Self {
self.default_retry_policy = policy;
self
}
pub fn with_default_timeout(mut self, timeout: Duration) -> Self {
self.default_timeout = timeout;
self
}
/// Get the retry policy for a step (step-specific or default)
pub fn get_retry_policy<'a>(&'a self, step: &'a StepDefinition) -> &'a RetryPolicy {
step.retry_policy
.as_ref()
.unwrap_or(&self.default_retry_policy)
}
/// Get the timeout for a step (step-specific or default)
pub fn get_timeout(&self, step: &StepDefinition) -> Duration {
step.timeout.unwrap_or(self.default_timeout)
}
/// Validate the workflow DAG structure and build dependency graph.
/// Returns an error if:
/// - A step depends on a non-existent step
/// - There's a cycle in the dependencies
///
/// On success, pre-computes reverse dependencies for O(1) dependent lookup.
pub fn validate(&mut self) -> Result<(), String> {
// Build HashMap for O(1) lookup instead of O(n) linear search
let steps_map: HashMap<&StepId, &StepDefinition> =
self.steps.iter().map(|s| (&s.id, s)).collect();
// Check all dependencies exist
for step in &self.steps {
for dep in &step.depends_on {
if !steps_map.contains_key(dep) {
return Err(format!(
"Step '{}' depends on non-existent step '{}'",
step.id, dep
));
}
}
}
// Check for cycles using DFS
let mut visited = HashSet::new();
let mut rec_stack = HashSet::new();
for step in &self.steps {
if !visited.contains(&step.id)
&& Self::has_cycle(&step.id, &steps_map, &mut visited, &mut rec_stack)
{
return Err(format!("Cycle detected involving step '{}'", step.id));
}
}
// Build reverse dependency map: for each step, which steps depend on it?
self.reverse_deps.clear();
for (idx, step) in self.steps.iter().enumerate() {
for dep_id in &step.depends_on {
self.reverse_deps
.entry(dep_id.clone())
.or_default()
.push(idx);
}
}
// Cache indices of steps with no dependencies (can start immediately)
self.initial_step_indices = self
.steps
.iter()
.enumerate()
.filter(|(_, s)| s.depends_on.is_empty())
.map(|(i, _)| i)
.collect();
Ok(())
}
/// DFS helper for cycle detection with O(1) HashMap lookup
fn has_cycle<'a>(
step_id: &'a StepId,
steps_map: &HashMap<&'a StepId, &'a StepDefinition>,
visited: &mut HashSet<&'a StepId>,
rec_stack: &mut HashSet<&'a StepId>,
) -> bool {
if rec_stack.contains(step_id) {
return true; // Back edge found - cycle!
}
if visited.contains(step_id) {
return false; // Already fully processed
}
visited.insert(step_id);
rec_stack.insert(step_id);
// O(1) lookup instead of linear search
if let Some(step) = steps_map.get(step_id) {
for dep in &step.depends_on {
if Self::has_cycle(dep, steps_map, visited, rec_stack) {
return true;
}
}
}
rec_stack.remove(step_id);
false
}
/// Get indices of steps that depend on the given step
pub fn get_dependent_indices(&self, step_id: &StepId) -> &[usize] {
self.reverse_deps
.get(step_id)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
/// Get indices of steps with no dependencies
pub fn get_initial_step_indices(&self) -> &[usize] {
&self.initial_step_indices
}
}