use std::sync::Arc;
use std::time::{Duration, Instant};
use a2a_protocol_server::builder::RequestHandlerBuilder;
use a2a_protocol_server::store::InMemoryTaskStore;
use a2a_protocol_server::{agent_executor, EventEmitter, RequestHandler};
use a2a_protocol_types::message::{Message, MessageId, MessageRole, Part};
use a2a_protocol_types::params::MessageSendParams;
use a2a_protocol_types::task::TaskState;
const LOAD_DURATION: Duration = Duration::from_secs(3);
const WARMUP_REQUESTS: usize = 50;
struct LoadAgent;
agent_executor!(LoadAgent, |ctx, queue| async {
let emit = EventEmitter::new(ctx, queue);
emit.status(TaskState::Working).await?;
emit.artifact("out", vec![Part::text("chunk")], None, Some(true))
.await?;
emit.status(TaskState::Completed).await?;
Ok(())
});
fn handler_with_capacity(max_tasks: usize) -> Arc<RequestHandler> {
let store = InMemoryTaskStore::with_config(a2a_protocol_server::store::TaskStoreConfig {
max_capacity: Some(max_tasks),
..Default::default()
});
Arc::new(
RequestHandlerBuilder::new(LoadAgent)
.with_task_store(store)
.build()
.expect("build handler"),
)
}
fn params(seq: usize) -> MessageSendParams {
MessageSendParams {
tenant: None,
message: Message {
id: MessageId::new(format!("m{seq}")),
role: MessageRole::User,
parts: vec![Part::text("ping")],
task_id: None,
context_id: None,
metadata: None,
extensions: None,
reference_task_ids: None,
},
configuration: None,
metadata: None,
}
}
async fn under_load<F, Fut>(handler: &Arc<RequestHandler>, probe: F) -> (usize, usize, usize)
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = usize>,
{
let mut sent = 0usize;
for _ in 0..WARMUP_REQUESTS {
let _ = handler.on_send_message(params(sent), false, None).await;
sent += 1;
}
let early = probe().await;
let deadline = Instant::now() + LOAD_DURATION;
while Instant::now() < deadline {
let _ = handler.on_send_message(params(sent), false, None).await;
sent += 1;
}
let late = probe().await;
eprintln!("sustained load: {sent} requests, probe {early} -> {late}");
(early, late, sent)
}
#[tokio::test(flavor = "multi_thread")]
async fn event_queues_do_not_accumulate_under_sustained_load() {
let handler = handler_with_capacity(10_000);
let probe_handler = Arc::clone(&handler);
let (early, late, sent) = under_load(&handler, || {
let h = Arc::clone(&probe_handler);
async move { h.active_queue_count().await }
})
.await;
assert!(
sent > WARMUP_REQUESTS * 2,
"the load loop must actually issue traffic; only {sent} requests completed"
);
assert!(
late <= early + 1,
"event queues grew from {early} to {late} across {sent} requests — \
queues are not being reclaimed as tasks finish"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn the_task_store_stays_bounded_under_sustained_load() {
const CAPACITY: usize = 200;
let handler = handler_with_capacity(CAPACITY);
let probe_handler = Arc::clone(&handler);
let (_early, late, sent) = under_load(&handler, || {
let h = Arc::clone(&probe_handler);
async move { usize::try_from(h.task_count().await.unwrap_or(0)).unwrap_or(usize::MAX) }
})
.await;
assert!(
sent > CAPACITY * 2,
"the load loop must overrun capacity for this to mean anything; \
only {sent} requests against a capacity of {CAPACITY}"
);
let ceiling = CAPACITY * 3;
assert!(
late <= ceiling,
"task store held {late} tasks after {sent} requests against a capacity \
of {CAPACITY} — eviction is not keeping the store bounded"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn cancellation_tokens_do_not_accumulate_under_sustained_load() {
let handler = handler_with_capacity(10_000);
let probe_handler = Arc::clone(&handler);
let (early, late, sent) = under_load(&handler, || {
let h = Arc::clone(&probe_handler);
async move { h.cancellation_token_count().await }
})
.await;
assert!(
late <= early + 1,
"cancellation tokens grew from {early} to {late} across {sent} requests — \
tokens are not being removed as tasks finish"
);
}