use std::collections::HashMap;
use std::sync::{Mutex, PoisonError};
use a2a_protocol_types::events::StreamResponse;
use tokio::sync::oneshot;
#[derive(Debug, Default)]
pub struct TerminalGate {
state: Mutex<GateState>,
}
#[derive(Debug, Default)]
struct GateState {
closed: bool,
waiting: HashMap<u64, oneshot::Sender<StreamResponse>>,
}
impl TerminalGate {
fn lock(&self) -> std::sync::MutexGuard<'_, GateState> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
pub fn arm(&self, seq: u64) -> Option<oneshot::Receiver<StreamResponse>> {
let mut state = self.lock();
if state.closed {
return None;
}
let (tx, rx) = oneshot::channel();
state.waiting.insert(seq, tx);
drop(state);
Some(rx)
}
pub fn disarm(&self, seq: u64) {
self.lock().waiting.remove(&seq);
}
pub fn resolve(&self, seq: u64, verdict: StreamResponse) {
let waiting = self.lock().waiting.remove(&seq);
if let Some(tx) = waiting {
let _ = tx.send(verdict);
}
}
pub fn close(&self) {
let mut state = self.lock();
state.closed = true;
state.waiting.clear();
}
}
#[derive(Debug)]
pub struct CloseOnDrop(pub std::sync::Arc<TerminalGate>);
impl Drop for CloseOnDrop {
fn drop(&mut self) {
self.0.close();
}
}
#[cfg(test)]
mod tests {
use super::*;
use a2a_protocol_types::events::TaskStatusUpdateEvent;
use a2a_protocol_types::task::{ContextId, TaskId, TaskState, TaskStatus};
fn status(state: TaskState) -> StreamResponse {
StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
task_id: TaskId::new("t"),
context_id: ContextId::new("c"),
status: TaskStatus::new(state),
metadata: None,
})
}
fn state_of(event: &StreamResponse) -> Option<TaskState> {
match event {
StreamResponse::StatusUpdate(e) => Some(e.status.state),
_ => None,
}
}
#[tokio::test]
async fn a_verdict_reaches_the_ticket_armed_for_its_seq() {
let gate = TerminalGate::default();
let mut first = gate.arm(1).expect("open gate arms");
let mut second = gate.arm(2).expect("open gate arms");
gate.resolve(2, status(TaskState::Canceled));
gate.resolve(1, status(TaskState::Completed));
assert_eq!(
state_of(&first.try_recv().expect("resolved")),
Some(TaskState::Completed)
);
assert_eq!(
state_of(&second.try_recv().expect("resolved")),
Some(TaskState::Canceled)
);
}
#[tokio::test]
async fn a_disarmed_or_unknown_ticket_is_ignored() {
let gate = TerminalGate::default();
let mut rx = gate.arm(7).expect("arms");
gate.disarm(7);
gate.resolve(7, status(TaskState::Completed));
assert!(
matches!(rx.try_recv(), Err(oneshot::error::TryRecvError::Closed)),
"a disarmed ticket is dropped, and receives nothing"
);
gate.resolve(8, status(TaskState::Completed));
assert!(gate.lock().waiting.is_empty());
}
#[tokio::test]
async fn closing_releases_waiters_and_stops_arming() {
let gate = std::sync::Arc::new(TerminalGate::default());
let mut rx = gate.arm(1).expect("arms");
drop(CloseOnDrop(std::sync::Arc::clone(&gate)));
assert!(
matches!(rx.try_recv(), Err(oneshot::error::TryRecvError::Closed)),
"a waiter is released by the close"
);
assert!(gate.arm(2).is_none(), "a closed gate arms nothing");
}
use crate::streaming::event_queue::{
EventQueueReader as _, EventQueueWriter as _, InMemoryQueueWriter,
new_in_memory_queue_with_options, new_in_memory_queue_with_persistence,
};
const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
fn gated() -> (
std::sync::Arc<InMemoryQueueWriter>,
crate::streaming::event_queue::InMemoryQueueReader,
tokio::sync::mpsc::Receiver<
a2a_protocol_types::error::A2aResult<crate::streaming::StreamEvent>,
>,
) {
let (writer, reader, rx) = new_in_memory_queue_with_persistence(8, 1 << 20, TIMEOUT);
(std::sync::Arc::new(writer.with_terminal_gate()), reader, rx)
}
async fn within<F: std::future::Future>(fut: F) -> F::Output {
tokio::time::timeout(std::time::Duration::from_secs(10), fut)
.await
.expect("did not finish in time")
}
async fn next_state(
reader: &mut crate::streaming::event_queue::InMemoryQueueReader,
) -> Option<TaskState> {
let frame = reader.read().await?.ok()?;
state_of(&frame.event)
}
#[tokio::test]
async fn a_terminal_event_is_broadcast_as_the_verdict() {
let (writer, mut reader, mut rx) = gated();
let gate = writer.terminal_gate().expect("gated");
let w = std::sync::Arc::clone(&writer);
let write = tokio::spawn(async move { w.write(status(TaskState::Completed)).await });
let handed = within(rx.recv()).await.expect("handed over").expect("ok");
assert!(!write.is_finished(), "held until the verdict");
gate.resolve(handed.seq.expect("positioned"), status(TaskState::Canceled));
within(write).await.expect("joins").expect("write succeeds");
assert_eq!(
within(next_state(&mut reader)).await,
Some(TaskState::Canceled)
);
}
#[tokio::test(start_paused = true)]
async fn no_verdict_in_time_broadcasts_the_event_unchanged() {
let (writer, mut reader, mut rx) = gated();
let w = std::sync::Arc::clone(&writer);
let write = tokio::spawn(async move { w.write(status(TaskState::Completed)).await });
let _handed = within(rx.recv()).await.expect("handed over");
within(write).await.expect("joins").expect("write succeeds");
assert_eq!(
within(next_state(&mut reader)).await,
Some(TaskState::Completed)
);
let gate = writer.terminal_gate().expect("gated");
assert!(gate.lock().waiting.is_empty(), "the ticket was withdrawn");
}
#[tokio::test]
async fn only_terminal_events_wait_and_only_while_someone_can_answer() {
let (writer, mut reader, mut rx) = gated();
writer
.write(status(TaskState::Working))
.await
.expect("not held");
assert!(within(rx.recv()).await.is_some());
assert_eq!(
within(next_state(&mut reader)).await,
Some(TaskState::Working)
);
drop(rx);
tokio::time::timeout(
std::time::Duration::from_secs(1),
writer.write(status(TaskState::Completed)),
)
.await
.expect("a closed channel does not wait out the timeout")
.expect("write succeeds");
assert_eq!(
within(next_state(&mut reader)).await,
Some(TaskState::Completed)
);
let gate = writer.terminal_gate().expect("gated");
assert!(gate.lock().waiting.is_empty(), "the ticket was withdrawn");
}
#[test]
fn a_gate_needs_a_persistence_channel_and_a_request() {
let (plain, _reader) = new_in_memory_queue_with_options(8, 1 << 20, TIMEOUT);
assert!(plain.with_terminal_gate().terminal_gate().is_none());
let (unasked, _reader, _rx) = new_in_memory_queue_with_persistence(8, 1 << 20, TIMEOUT);
assert!(unasked.terminal_gate().is_none());
}
}