use std::sync::Arc;
use a2a_protocol_types::error::A2aResult;
use a2a_protocol_types::task::TaskId;
use super::super::RequestHandler;
use super::decisions::second_send_blocked;
use crate::error::{ServerError, ServerResult};
use crate::streaming::{InMemoryQueueReader, InMemoryQueueWriter, QueueLease};
pub(super) type LeasedQueue = (
Arc<InMemoryQueueWriter>,
InMemoryQueueReader,
Option<tokio::sync::mpsc::Receiver<A2aResult<crate::streaming::StreamEvent>>>,
);
impl RequestHandler {
pub(super) async fn reject_in_flight_send(&self, task_id: &TaskId) -> ServerResult<()> {
let blocked = self
.cancellation_tokens
.read()
.await
.get(task_id)
.is_some_and(second_send_blocked);
if blocked {
return Err(ServerError::UnsupportedOperation(format!(
"task {task_id} is already being processed; \
wait for it to reach input-required or a terminal state before sending again"
)));
}
Ok(())
}
pub(super) async fn lease_event_queue(
&self,
task_id: &TaskId,
use_background: bool,
) -> ServerResult<LeasedQueue> {
let capacity = self
.tenant_limits()
.and_then(|limits| limits.event_queue_capacity);
match self
.event_queue_manager
.lease(task_id, use_background, capacity)
.await
{
QueueLease::Created {
writer,
reader,
persistence_rx,
} => {
if self.task_store.supports_event_log() {
match self.task_store.last_event_seq(task_id).await {
Ok(seq) => writer.seed_seq(seq),
Err(e) => {
drop((writer, reader, persistence_rx));
self.event_queue_manager.destroy(task_id).await;
return Err(ServerError::Internal(format!(
"task {task_id}: could not read the event log's last \
position, so this turn's events could not be numbered \
without colliding with the previous turn's: {e}"
)));
}
}
}
Ok((writer, reader, persistence_rx))
}
QueueLease::Existing => Err(ServerError::UnsupportedOperation(format!(
"task {task_id} is already being processed; wait for it to reach \
input-required or a terminal state before sending again"
))),
QueueLease::CapacityExhausted => {
let cap = self
.event_queue_manager
.max_concurrent_queues()
.map_or_else(String::new, |n| format!(" ({n})"));
Err(ServerError::Overloaded(format!(
"server at maximum concurrent stream capacity{cap}; retry later"
)))
}
}
}
}
#[cfg(test)]
mod tests {
use std::future::Future;
use std::pin::Pin;
use a2a_protocol_types::error::{A2aError, A2aResult};
use a2a_protocol_types::params::ListTasksParams;
use a2a_protocol_types::responses::TaskListResponse;
use a2a_protocol_types::task::{Task, TaskId};
use crate::builder::RequestHandlerBuilder;
use crate::error::ServerError;
use crate::executor::AgentExecutor;
use crate::request_context::RequestContext;
use crate::store::TaskStore;
use crate::streaming::EventQueueWriter;
struct NoopExecutor;
impl AgentExecutor for NoopExecutor {
fn execute<'a>(
&'a self,
_ctx: &'a RequestContext,
_queue: &'a dyn EventQueueWriter,
) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
Box::pin(async { Ok(()) })
}
}
struct UnreadableLogStore;
impl TaskStore for UnreadableLogStore {
fn save<'a>(
&'a self,
_t: &'a Task,
) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
Box::pin(async { Ok(()) })
}
fn get<'a>(
&'a self,
_id: &'a TaskId,
) -> Pin<Box<dyn Future<Output = A2aResult<Option<Task>>> + Send + 'a>> {
Box::pin(async { Ok(None) })
}
fn list<'a>(
&'a self,
_p: &'a ListTasksParams,
) -> Pin<Box<dyn Future<Output = A2aResult<TaskListResponse>> + Send + 'a>> {
Box::pin(async { Ok(TaskListResponse::new(vec![])) })
}
fn insert_if_absent<'a>(
&'a self,
_t: &'a Task,
) -> Pin<Box<dyn Future<Output = A2aResult<bool>> + Send + 'a>> {
Box::pin(async { Ok(true) })
}
fn delete<'a>(
&'a self,
_id: &'a TaskId,
) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
Box::pin(async { Ok(()) })
}
fn supports_event_log(&self) -> bool {
true
}
fn last_event_seq<'a>(
&'a self,
_id: &'a TaskId,
) -> Pin<Box<dyn Future<Output = A2aResult<u64>> + Send + 'a>> {
Box::pin(async { Err(A2aError::internal("the pool is gone")) })
}
}
#[tokio::test]
async fn an_unreadable_log_position_refuses_the_lease_rather_than_renumbering() {
let handler = RequestHandlerBuilder::new(NoopExecutor)
.with_task_store(UnreadableLogStore)
.build()
.expect("handler");
let task_id = TaskId("t-1".to_owned());
let err = handler
.lease_event_queue(&task_id, true)
.await
.expect_err("an unreadable log position must refuse the lease");
assert!(
matches!(err, ServerError::Internal(_)),
"expected an internal error, got {err:?}"
);
assert_eq!(
handler.event_queue_manager.active_count().await,
0,
"the failed lease must leave no queue behind"
);
}
}