use adk_graph::edge::{END, START};
use adk_graph::graph::StateGraph;
use adk_graph::node::{ExecutionConfig, NodeOutput};
use adk_graph::state::State;
use serde_json::json;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
#[derive(Default)]
struct Gauge {
current: AtomicUsize,
peak: AtomicUsize,
}
impl Gauge {
fn enter(&self) {
let now = self.current.fetch_add(1, Ordering::SeqCst) + 1;
self.peak.fetch_max(now, Ordering::SeqCst);
}
fn leave(&self) {
self.current.fetch_sub(1, Ordering::SeqCst);
}
fn peak(&self) -> usize {
self.peak.load(Ordering::SeqCst)
}
}
fn fan_out(width: usize, gauge: Arc<Gauge>) -> StateGraph {
let mut graph = StateGraph::with_channels(&["done"]);
for index in 0..width {
let name = format!("n{index}");
let gauge = Arc::clone(&gauge);
graph = graph.add_node_fn(&name, move |_ctx| {
let gauge = Arc::clone(&gauge);
async move {
gauge.enter();
tokio::time::sleep(Duration::from_millis(40)).await;
gauge.leave();
Ok(NodeOutput::new().with_update("done", json!(true)))
}
});
}
for index in 0..width {
let name = format!("n{index}");
graph = graph.add_edge(START, &name).add_edge(&name, END);
}
graph
}
#[tokio::test]
async fn concurrency_never_exceeds_the_limit() {
let gauge = Arc::new(Gauge::default());
let graph = fan_out(12, Arc::clone(&gauge)).compile().unwrap().with_max_concurrency(3);
graph.invoke(State::new(), ExecutionConfig::new("bounded")).await.unwrap();
let peak = gauge.peak();
assert!(peak <= 3, "peak concurrency was {peak}, which exceeds the limit of 3");
assert!(peak > 1, "the limit must still allow parallelism, but peak was {peak}");
}
#[tokio::test]
async fn without_a_limit_the_whole_frontier_runs_at_once() {
let gauge = Arc::new(Gauge::default());
let graph = fan_out(12, Arc::clone(&gauge)).compile().unwrap();
graph.invoke(State::new(), ExecutionConfig::new("unbounded")).await.unwrap();
assert_eq!(
gauge.peak(),
12,
"an unconfigured graph must keep running its whole frontier concurrently"
);
}
#[tokio::test]
async fn a_limit_above_the_frontier_width_is_inert() {
let gauge = Arc::new(Gauge::default());
let graph = fan_out(4, Arc::clone(&gauge)).compile().unwrap().with_max_concurrency(50);
graph.invoke(State::new(), ExecutionConfig::new("wide-limit")).await.unwrap();
assert_eq!(gauge.peak(), 4);
}
#[tokio::test]
async fn a_limit_of_one_serialises_the_frontier() {
let gauge = Arc::new(Gauge::default());
let graph = fan_out(5, Arc::clone(&gauge)).compile().unwrap().with_max_concurrency(1);
graph.invoke(State::new(), ExecutionConfig::new("serial")).await.unwrap();
assert_eq!(gauge.peak(), 1);
}
#[test]
fn prop_concurrency_respects_the_bound() {
use proptest::prelude::*;
proptest!(ProptestConfig::with_cases(16), |(width in 2usize..10, limit in 1usize..6)| {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.expect("runtime");
let peak = runtime.block_on(async {
let gauge = Arc::new(Gauge::default());
let graph =
fan_out(width, Arc::clone(&gauge)).compile().unwrap().with_max_concurrency(limit);
graph.invoke(State::new(), ExecutionConfig::new("prop")).await.unwrap();
gauge.peak()
});
prop_assert!(peak <= limit, "peak {} exceeded limit {}", peak, limit);
});
}