use std::sync::Arc;
use std::time::{Duration, Instant};
use a2a_protocol_types::error::A2aResult;
use a2a_protocol_types::message::{Message, MessageId, MessageRole, Part};
use a2a_protocol_types::params::MessageSendParams;
use crate::builder::RequestHandlerBuilder;
use crate::error::ServerError;
use crate::executor::AgentExecutor;
use crate::handler::RequestHandler;
use crate::request_context::RequestContext;
use crate::streaming::EventQueueWriter;
use crate::tenant_config::{PerTenantConfig, TenantLimits};
struct SleepyExecutor(Duration);
impl AgentExecutor for SleepyExecutor {
fn execute<'a>(
&'a self,
_ctx: &'a RequestContext,
_queue: &'a dyn EventQueueWriter,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = A2aResult<()>> + Send + 'a>> {
let nap = self.0;
Box::pin(async move {
tokio::time::sleep(nap).await;
Ok(())
})
}
}
fn params_for(tenant: &str) -> MessageSendParams {
MessageSendParams {
message: Message {
id: MessageId::new("msg-1"),
role: MessageRole::User,
parts: vec![Part::text("hello")],
context_id: None,
task_id: None,
reference_task_ids: None,
extensions: None,
metadata: None,
},
configuration: None,
metadata: None,
tenant: Some(tenant.to_owned()),
}
}
fn config_with(tenant: &str, limits: TenantLimits) -> PerTenantConfig {
PerTenantConfig::builder()
.with_override(tenant, limits)
.build()
}
#[tokio::test]
async fn a_tenant_executor_timeout_overrides_the_handlers() {
let handler = RequestHandlerBuilder::new(SleepyExecutor(Duration::from_secs(60)))
.with_executor_timeout(Duration::from_secs(30))
.with_tenant_config(config_with(
"impatient",
TenantLimits::builder()
.executor_timeout(Duration::from_millis(50))
.build(),
))
.build()
.expect("build");
let started = Instant::now();
let result = handler
.on_send_message(params_for("impatient"), false, None)
.await;
assert!(result.is_ok(), "a timed-out executor still yields a task");
assert!(
started.elapsed() < Duration::from_secs(5),
"the tenant asked for 50ms; returning after {:?} means the handler's \
30s was used instead",
started.elapsed()
);
}
#[tokio::test]
async fn a_tenant_without_an_override_keeps_the_handlers_timeout() {
let handler = RequestHandlerBuilder::new(SleepyExecutor(Duration::from_secs(60)))
.with_executor_timeout(Duration::from_millis(50))
.with_tenant_config(config_with(
"quiet",
TenantLimits::builder().max_concurrent_tasks(4).build(),
))
.build()
.expect("build");
let started = Instant::now();
let result = handler
.on_send_message(params_for("quiet"), false, None)
.await;
assert!(result.is_ok());
assert!(
started.elapsed() < Duration::from_secs(5),
"the handler's 50ms must still apply when the tenant declares none; \
took {:?}",
started.elapsed()
);
}
async fn saturate(handler: &Arc<RequestHandler>, tenant: &str, n: usize) {
for _ in 0..n {
let h = Arc::clone(handler);
let t = tenant.to_owned();
tokio::spawn(async move { h.on_send_message(params_for(&t), false, None).await });
}
tokio::time::sleep(Duration::from_millis(200)).await;
}
fn capped_handler(tenant: &str, cap: usize) -> Arc<RequestHandler> {
Arc::new(
RequestHandlerBuilder::new(SleepyExecutor(Duration::from_secs(60)))
.with_tenant_config(config_with(
tenant,
TenantLimits::builder().max_concurrent_tasks(cap).build(),
))
.build()
.expect("build"),
)
}
#[tokio::test]
async fn a_tenant_at_its_concurrency_limit_is_refused() {
let handler = capped_handler("busy", 2);
saturate(&handler, "busy", 2).await;
let err = handler
.on_send_message(params_for("busy"), false, None)
.await
.expect_err("the third concurrent send must be refused");
assert!(
matches!(err, ServerError::Overloaded(_)),
"expected Overloaded, got {err:?}"
);
}
#[tokio::test]
async fn a_refused_request_leaves_no_task_behind() {
let handler = capped_handler("clean", 1);
saturate(&handler, "clean", 1).await;
let before = handler.task_store.count().await.expect("count");
let _ = handler
.on_send_message(params_for("clean"), false, None)
.await;
let after = handler.task_store.count().await.expect("count");
assert_eq!(
before, after,
"a refused send must not persist a task; store went {before} -> {after}"
);
}
#[tokio::test]
async fn the_slot_is_released_when_the_executor_finishes() {
let handler = Arc::new(
RequestHandlerBuilder::new(SleepyExecutor(Duration::from_millis(10)))
.with_tenant_config(config_with(
"serial",
TenantLimits::builder().max_concurrent_tasks(1).build(),
))
.build()
.expect("build"),
);
for attempt in 0..3 {
handler
.on_send_message(params_for("serial"), false, None)
.await
.unwrap_or_else(|e| panic!("send {attempt} was refused: {e:?} — a permit leaked"));
}
}
#[tokio::test]
async fn the_concurrency_limit_is_per_tenant_not_global() {
let handler = Arc::new(
RequestHandlerBuilder::new(SleepyExecutor(Duration::from_secs(60)))
.with_tenant_config(
PerTenantConfig::builder()
.with_override(
"loud",
TenantLimits::builder().max_concurrent_tasks(1).build(),
)
.with_override(
"quiet",
TenantLimits::builder().max_concurrent_tasks(1).build(),
)
.build(),
)
.build()
.expect("build"),
);
saturate(&handler, "loud", 1).await;
let started = Instant::now();
let result = tokio::time::timeout(
Duration::from_secs(5),
handler.on_send_message(params_for("quiet"), false, None),
)
.await;
match result {
Ok(Ok(_)) | Err(_) => {
let _ = started;
}
Ok(Err(e)) => panic!("a different tenant must not be blocked, got {e:?}"),
}
}
#[tokio::test]
async fn no_limit_configured_means_no_ceiling() {
let handler = Arc::new(
RequestHandlerBuilder::new(SleepyExecutor(Duration::from_secs(60)))
.build()
.expect("build"),
);
saturate(&handler, "anyone", 3).await;
let result = tokio::time::timeout(
Duration::from_secs(5),
handler.on_send_message(params_for("anyone"), false, None),
)
.await;
assert!(
!matches!(result, Ok(Err(ServerError::Overloaded(_)))),
"no tenant config must mean no concurrency ceiling"
);
}