fix(graph): bound concurrency caps by the semaphore permit limit
Semaphore::new panics above tokio's MAX_PERMITS, so a fixed or templated map cap (or settings.max_concurrency) that large crashed the run instead of erroring. The validator rejects both at load, resolve_max_concurrency rejects every arm at run time, and the executor checks the graph setting before building its own semaphore, so validation mode makes no difference. Also corrects run_child_agent_with_graph_inputs' contract comment: inputs for a config-only agent are rejected, not ignored.
This commit is contained in:
4 files changed
+134
-3
No files matched your search
@@ -8,6 +8,7 @@ use anyhow::{Result, bail};
|
||||
use std::collections::{BTreeMap, HashSet, VecDeque};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ValidationError {
|
||||
@@ -537,6 +538,12 @@ impl GraphValidator {
|
||||
deadlock the executor",
|
||||
));
|
||||
}
|
||||
if graph.settings.max_concurrency > Semaphore::MAX_PERMITS {
|
||||
result.error(ValidationError::new(format!(
|
||||
"settings.max_concurrency must be <= {} (got {}); the runtime cannot allocate more permits",
|
||||
Semaphore::MAX_PERMITS, graph.settings.max_concurrency
|
||||
)));
|
||||
}
|
||||
|
||||
for (node_id, node) in &graph.nodes {
|
||||
if let NodeType::Map(m) = &node.node_type
|
||||
@@ -548,6 +555,18 @@ impl GraphValidator {
|
||||
would deadlock the executor",
|
||||
));
|
||||
}
|
||||
if let NodeType::Map(m) = &node.node_type
|
||||
&& let Some(ConcurrencyCap::Fixed(n)) = m.max_concurrency
|
||||
&& n > Semaphore::MAX_PERMITS
|
||||
{
|
||||
result.error(ValidationError::with_node(
|
||||
node_id,
|
||||
format!(
|
||||
"map node's `max_concurrency` must be <= {} (got {n}); the runtime cannot allocate more permits",
|
||||
Semaphore::MAX_PERMITS
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3158,6 +3177,24 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_max_concurrency_above_semaphore_limit_errors() {
|
||||
let mut graph = graph_with(vec![("e", end_node("e"))], "e");
|
||||
graph.settings.max_concurrency = Semaphore::MAX_PERMITS + 1;
|
||||
|
||||
let result = validator().validate(&graph);
|
||||
|
||||
assert!(
|
||||
result.errors.iter().any(|e| e.message.contains(&format!(
|
||||
"settings.max_concurrency must be <= {} (got {})",
|
||||
Semaphore::MAX_PERMITS,
|
||||
Semaphore::MAX_PERMITS + 1
|
||||
))),
|
||||
"expected graph-level max_concurrency upper-bound error: {:?}",
|
||||
result.errors
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_max_concurrency_default_is_valid() {
|
||||
let graph = graph_with(vec![("e", end_node("e"))], "e");
|
||||
@@ -3458,6 +3495,30 @@ mod tests {
|
||||
assert_eq!(w[1].node_id.as_deref(), Some("l"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_max_concurrency_above_semaphore_limit_errors() {
|
||||
let mut map = map_node_basic("m", "br", Some("end"));
|
||||
if let NodeType::Map(ref mut mm) = map.node_type {
|
||||
mm.max_concurrency = Some(ConcurrencyCap::Fixed(Semaphore::MAX_PERMITS + 1));
|
||||
}
|
||||
let branch = llm_with_state_updates("br", &[("output", "{{output}}")], None);
|
||||
let graph = graph_with(
|
||||
vec![("m", map), ("br", branch), ("end", end_node("end"))],
|
||||
"m",
|
||||
);
|
||||
|
||||
let result = validator().validate(&graph);
|
||||
|
||||
assert!(
|
||||
result.errors.iter().any(|e| e
|
||||
.message
|
||||
.contains(&format!("must be <= {}", Semaphore::MAX_PERMITS))
|
||||
&& e.node_id.as_deref() == Some("m")),
|
||||
"expected map max_concurrency upper-bound error: {:?}",
|
||||
result.errors
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_max_concurrency_zero_errors() {
|
||||
let mut map = map_node_basic("m", "br", Some("end"));
|
||||
|
||||
Reference in new issue
Block a user