1use std::collections::HashMap;
9use std::sync::Arc;
10
11use a2a_protocol_types::task::TaskId;
12use tokio::sync::RwLock;
13
14use a2a_protocol_types::error::A2aResult;
15use a2a_protocol_types::events::StreamResponse;
16
17use super::{
18 DEFAULT_MAX_EVENT_SIZE, DEFAULT_QUEUE_CAPACITY, DEFAULT_WRITE_TIMEOUT, InMemoryQueueReader,
19 InMemoryQueueWriter, StreamEvent, new_in_memory_queue_with_options,
20 new_in_memory_queue_with_persistence,
21};
22use crate::metrics::Metrics;
23
24#[allow(clippy::large_enum_variant)]
39pub enum QueueLease {
40 Created {
43 writer: Arc<InMemoryQueueWriter>,
44 reader: InMemoryQueueReader,
45 persistence_rx: Option<tokio::sync::mpsc::Receiver<A2aResult<StreamEvent>>>,
46 },
47 Existing,
52 CapacityExhausted,
55}
56
57#[derive(Clone)]
65pub struct EventQueueManager {
66 writers: Arc<RwLock<HashMap<TaskId, Arc<InMemoryQueueWriter>>>>,
67 capacity: usize,
69 max_event_size: usize,
71 write_timeout: std::time::Duration,
73 max_concurrent_queues: Option<usize>,
75 metrics: Option<Arc<dyn Metrics>>,
77}
78
79impl std::fmt::Debug for EventQueueManager {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 f.debug_struct("EventQueueManager")
82 .field("writers", &"<RwLock<HashMap<...>>>")
83 .field("capacity", &self.capacity)
84 .field("max_event_size", &self.max_event_size)
85 .field("write_timeout", &self.write_timeout)
86 .field("max_concurrent_queues", &self.max_concurrent_queues)
87 .field("metrics", &self.metrics.is_some())
88 .finish()
89 }
90}
91
92impl Default for EventQueueManager {
93 fn default() -> Self {
94 Self {
95 writers: Arc::default(),
96 capacity: DEFAULT_QUEUE_CAPACITY,
97 max_event_size: DEFAULT_MAX_EVENT_SIZE,
98 write_timeout: DEFAULT_WRITE_TIMEOUT,
99 max_concurrent_queues: None,
100 metrics: None,
101 }
102 }
103}
104
105impl EventQueueManager {
106 #[must_use]
116 pub fn new() -> Self {
117 Self::default()
118 }
119
120 #[must_use]
122 pub fn with_capacity(capacity: usize) -> Self {
123 Self {
124 writers: Arc::default(),
125 capacity,
126 max_event_size: DEFAULT_MAX_EVENT_SIZE,
127 write_timeout: DEFAULT_WRITE_TIMEOUT,
128 max_concurrent_queues: None,
129 metrics: None,
130 }
131 }
132
133 #[must_use]
138 pub const fn with_max_event_size(mut self, max_event_size: usize) -> Self {
139 self.max_event_size = max_event_size;
140 self
141 }
142
143 #[must_use]
145 pub fn with_metrics(mut self, metrics: Arc<dyn Metrics>) -> Self {
146 self.metrics = Some(metrics);
147 self
148 }
149
150 fn observed(&self, writer: InMemoryQueueWriter) -> InMemoryQueueWriter {
153 match &self.metrics {
154 Some(metrics) => writer.with_metrics(Arc::clone(metrics)),
155 None => writer,
156 }
157 }
158
159 #[must_use]
164 pub const fn with_max_concurrent_queues(mut self, max: usize) -> Self {
165 self.max_concurrent_queues = Some(max);
166 self
167 }
168
169 pub async fn get_or_create(
180 &self,
181 task_id: &TaskId,
182 ) -> (Arc<InMemoryQueueWriter>, Option<InMemoryQueueReader>) {
183 let mut map = self.writers.write().await;
184 #[allow(clippy::option_if_let_else)]
185 let result = if let Some(existing) = map.get(task_id) {
186 (Arc::clone(existing), None)
187 } else if self
188 .max_concurrent_queues
189 .is_some_and(|max| map.len() >= max)
190 {
191 let (writer, _reader) = new_in_memory_queue_with_options(
194 self.capacity,
195 self.max_event_size,
196 self.write_timeout,
197 );
198 (Arc::new(writer), None)
199 } else {
200 let (writer, reader) = new_in_memory_queue_with_options(
201 self.capacity,
202 self.max_event_size,
203 self.write_timeout,
204 );
205 let writer = Arc::new(writer);
206 map.insert(task_id.clone(), Arc::clone(&writer));
207 (writer, Some(reader))
208 };
209 let queue_count = map.len();
210 drop(map);
211 if let Some(ref metrics) = self.metrics {
212 metrics.on_queue_depth_change(queue_count);
213 }
214 result
215 }
216
217 pub async fn get_or_create_with_persistence(
225 &self,
226 task_id: &TaskId,
227 ) -> (
228 Arc<InMemoryQueueWriter>,
229 Option<InMemoryQueueReader>,
230 Option<tokio::sync::mpsc::Receiver<A2aResult<StreamEvent>>>,
231 ) {
232 let mut map = self.writers.write().await;
233 #[allow(clippy::option_if_let_else)]
234 let result = if let Some(existing) = map.get(task_id) {
235 (Arc::clone(existing), None, None)
236 } else if self
237 .max_concurrent_queues
238 .is_some_and(|max| map.len() >= max)
239 {
240 let (writer, _reader) = new_in_memory_queue_with_options(
241 self.capacity,
242 self.max_event_size,
243 self.write_timeout,
244 );
245 (Arc::new(writer), None, None)
246 } else {
247 let (writer, reader, persistence_rx) = new_in_memory_queue_with_persistence(
248 self.capacity,
249 self.max_event_size,
250 self.write_timeout,
251 );
252 let writer = Arc::new(self.observed(writer));
253 map.insert(task_id.clone(), Arc::clone(&writer));
254 (writer, Some(reader), Some(persistence_rx))
255 };
256 let queue_count = map.len();
257 drop(map);
258 if let Some(ref metrics) = self.metrics {
259 metrics.on_queue_depth_change(queue_count);
260 }
261 result
262 }
263
264 #[allow(clippy::option_if_let_else)]
282 pub(crate) async fn lease(
283 &self,
284 task_id: &TaskId,
285 with_persistence: bool,
286 capacity: Option<usize>,
287 ) -> QueueLease {
288 let capacity = capacity.unwrap_or(self.capacity);
289 let mut map = self.writers.write().await;
290 let lease = if map.contains_key(task_id) {
291 QueueLease::Existing
292 } else if self
293 .max_concurrent_queues
294 .is_some_and(|max| map.len() >= max)
295 {
296 QueueLease::CapacityExhausted
297 } else if with_persistence {
298 let (writer, reader, persistence_rx) = new_in_memory_queue_with_persistence(
299 capacity,
300 self.max_event_size,
301 self.write_timeout,
302 );
303 let writer = Arc::new(self.observed(writer));
304 map.insert(task_id.clone(), Arc::clone(&writer));
305 QueueLease::Created {
306 writer,
307 reader,
308 persistence_rx: Some(persistence_rx),
309 }
310 } else {
311 let (writer, reader) =
312 new_in_memory_queue_with_options(capacity, self.max_event_size, self.write_timeout);
313 let writer = Arc::new(writer);
314 map.insert(task_id.clone(), Arc::clone(&writer));
315 QueueLease::Created {
316 writer,
317 reader,
318 persistence_rx: None,
319 }
320 };
321 let queue_count = map.len();
322 drop(map);
323 if let Some(ref metrics) = self.metrics {
324 metrics.on_queue_depth_change(queue_count);
325 }
326 lease
327 }
328
329 pub(crate) async fn current_seq(&self, task_id: &TaskId) -> Option<u64> {
348 let map = self.writers.read().await;
349 map.get(task_id).map(|w| w.current_seq())
350 }
351
352 pub(crate) async fn writer_for_cancel(&self, task_id: &TaskId) -> Arc<InMemoryQueueWriter> {
353 {
354 let map = self.writers.read().await;
355 if let Some(writer) = map.get(task_id) {
356 return Arc::clone(writer);
357 }
358 }
359 let (writer, _reader) = new_in_memory_queue_with_options(
360 self.capacity,
361 self.max_event_size,
362 self.write_timeout,
363 );
364 Arc::new(writer)
365 }
366
367 pub async fn subscribe(&self, task_id: &TaskId) -> Option<InMemoryQueueReader> {
375 let map = self.writers.read().await;
376 map.get(task_id).map(|writer| writer.subscribe())
377 }
378
379 pub(crate) async fn raw_subscribe(
386 &self,
387 task_id: &TaskId,
388 ) -> Option<tokio::sync::broadcast::Receiver<A2aResult<StreamEvent>>> {
389 let map = self.writers.read().await;
390 map.get(task_id).map(|writer| writer.raw_subscribe())
391 }
392
393 pub async fn subscribe_with_snapshot(
402 &self,
403 task_id: &TaskId,
404 snapshot: StreamResponse,
405 ) -> Option<InMemoryQueueReader> {
406 let map = self.writers.read().await;
407 let writer = map.get(task_id)?;
408 let rx = writer.raw_subscribe();
412 drop(map);
413 Some(InMemoryQueueReader::with_first_event(rx, snapshot))
414 }
415
416 pub async fn destroy(&self, task_id: &TaskId) {
418 let mut map = self.writers.write().await;
419 map.remove(task_id);
420 let queue_count = map.len();
421 drop(map);
422 if let Some(ref metrics) = self.metrics {
423 metrics.on_queue_depth_change(queue_count);
424 }
425 }
426
427 pub async fn active_count(&self) -> usize {
429 let map = self.writers.read().await;
430 map.len()
431 }
432
433 pub(crate) async fn has_queue(&self, task_id: &TaskId) -> bool {
439 self.writers.read().await.contains_key(task_id)
440 }
441
442 #[must_use]
445 pub(crate) const fn max_concurrent_queues(&self) -> Option<usize> {
446 self.max_concurrent_queues
447 }
448
449 pub async fn destroy_all(&self) {
451 let mut map = self.writers.write().await;
452 map.clear();
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459 use crate::streaming::event_queue::{EventQueueReader, EventQueueWriter};
460 use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
461 use a2a_protocol_types::task::{ContextId, TaskState, TaskStatus};
462
463 fn make_status_event(task_id: &str, state: TaskState) -> StreamResponse {
465 StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
466 task_id: TaskId::new(task_id),
467 context_id: ContextId::new("ctx-test"),
468 status: TaskStatus {
469 state,
470 message: None,
471 timestamp: None,
472 },
473 metadata: None,
474 })
475 }
476
477 #[test]
480 fn max_concurrent_queues_reports_configured_limit() {
481 assert_eq!(EventQueueManager::new().max_concurrent_queues(), None);
483 assert_eq!(
485 EventQueueManager::new()
486 .with_max_concurrent_queues(42)
487 .max_concurrent_queues(),
488 Some(42)
489 );
490 }
491
492 #[tokio::test]
493 async fn manager_get_or_create_new_task() {
494 let manager = EventQueueManager::new();
495 let task_id = TaskId::new("task-1");
496
497 let (writer, reader) = manager.get_or_create(&task_id).await;
498 assert!(
499 reader.is_some(),
500 "first get_or_create should return a reader"
501 );
502
503 writer
505 .write(make_status_event("task-1", TaskState::Working))
506 .await
507 .expect("write through manager writer should succeed");
508
509 assert_eq!(
510 manager.active_count().await,
511 1,
512 "should have 1 active queue"
513 );
514 }
515
516 #[tokio::test]
517 async fn manager_get_or_create_existing_task_returns_no_reader() {
518 let manager = EventQueueManager::new();
519 let task_id = TaskId::new("task-1");
520
521 let (_w1, r1) = manager.get_or_create(&task_id).await;
522 assert!(r1.is_some(), "first call should return a reader");
523
524 let (_w2, r2) = manager.get_or_create(&task_id).await;
525 assert!(
526 r2.is_none(),
527 "second call for same task should return None reader"
528 );
529
530 assert_eq!(
531 manager.active_count().await,
532 1,
533 "should still have only 1 active queue"
534 );
535 }
536
537 #[tokio::test]
538 async fn manager_subscribe_existing_task() {
539 use crate::streaming::event_queue::EventQueueReader;
540
541 let manager = EventQueueManager::new();
542 let task_id = TaskId::new("task-1");
543
544 let (writer, _reader) = manager.get_or_create(&task_id).await;
545
546 let sub = manager.subscribe(&task_id).await;
547 assert!(
548 sub.is_some(),
549 "subscribe should return a reader for existing task"
550 );
551
552 let mut sub_reader = sub.unwrap();
553 writer
554 .write(make_status_event("task-1", TaskState::Working))
555 .await
556 .expect("write should succeed");
557 drop(writer);
558
559 let r = sub_reader.read().await;
560 assert!(r.is_some(), "subscriber should receive the event");
561 }
562
563 #[tokio::test]
564 async fn manager_subscribe_nonexistent_task_returns_none() {
565 let manager = EventQueueManager::new();
566 let task_id = TaskId::new("no-such-task");
567
568 let sub = manager.subscribe(&task_id).await;
569 assert!(
570 sub.is_none(),
571 "subscribe should return None for nonexistent task"
572 );
573 }
574
575 #[tokio::test]
576 async fn manager_destroy_removes_queue() {
577 let manager = EventQueueManager::new();
578 let task_id = TaskId::new("task-1");
579
580 let (_writer, _reader) = manager.get_or_create(&task_id).await;
581 assert_eq!(manager.active_count().await, 1);
582
583 manager.destroy(&task_id).await;
584 assert_eq!(
585 manager.active_count().await,
586 0,
587 "destroy should remove the queue"
588 );
589 }
590
591 #[tokio::test]
592 async fn manager_destroy_all_clears_queues() {
593 let manager = EventQueueManager::new();
594
595 let _q1 = manager.get_or_create(&TaskId::new("t1")).await;
596 let _q2 = manager.get_or_create(&TaskId::new("t2")).await;
597 assert_eq!(manager.active_count().await, 2);
598
599 manager.destroy_all().await;
600 assert_eq!(
601 manager.active_count().await,
602 0,
603 "destroy_all should clear all queues"
604 );
605 }
606
607 #[tokio::test]
608 async fn lease_reports_existing_and_has_queue() {
609 let manager = EventQueueManager::new();
610 let task = TaskId::new("t-lease");
611
612 assert!(matches!(
614 manager.lease(&task, true, None).await,
615 QueueLease::Created { .. }
616 ));
617 assert!(manager.has_queue(&task).await, "queue should now be live");
618
619 assert!(matches!(
622 manager.lease(&task, true, None).await,
623 QueueLease::Existing
624 ));
625
626 assert!(!manager.has_queue(&TaskId::new("other")).await);
628 }
629
630 #[tokio::test]
631 async fn manager_max_concurrent_queues_enforced() {
632 let manager = EventQueueManager::new().with_max_concurrent_queues(1);
633
634 let (_w1, r1) = manager.get_or_create(&TaskId::new("t1")).await;
635 assert!(r1.is_some(), "first queue should be created successfully");
636
637 let (_w2, r2) = manager.get_or_create(&TaskId::new("t2")).await;
639 assert!(
640 r2.is_none(),
641 "second queue should return None reader when limit is reached"
642 );
643 assert_eq!(
644 manager.active_count().await,
645 1,
646 "should still have only 1 queue (second was not stored)"
647 );
648 }
649
650 #[tokio::test]
651 async fn manager_with_capacity_and_max_event_size() {
652 let manager = EventQueueManager::with_capacity(4).with_max_event_size(10); let task_id = TaskId::new("t1");
655 let (writer, _reader) = manager.get_or_create(&task_id).await;
656
657 let event = make_status_event("t1", TaskState::Working);
658 let result = writer.write(event).await;
659 assert!(
660 result.is_err(),
661 "event should be rejected by the size limit configured on the manager"
662 );
663 }
664
665 #[tokio::test]
681 async fn with_capacity_uses_the_given_capacity_not_the_default() {
682 let manager = EventQueueManager::with_capacity(1);
683 let task_id = TaskId::new("cap");
684 let (writer, reader) = manager.get_or_create(&task_id).await;
685 let mut reader = reader.expect("first get_or_create yields a reader");
686
687 writer
689 .write(make_status_event("cap", TaskState::Working))
690 .await
691 .expect("first write");
692 writer
693 .write(make_status_event("cap", TaskState::Completed))
694 .await
695 .expect("second write");
696
697 let first = reader.read().await.expect("reader is still open");
698 assert!(
699 first.is_err(),
700 "a capacity-1 queue must surface an overrun to the reader; \
701 got Ok, which is what DEFAULT_QUEUE_CAPACITY (256) would give"
702 );
703 }
704
705 #[tokio::test]
714 async fn lease_capacity_override_beats_the_managers_own() {
715 let manager = EventQueueManager::new();
716 let task_id = TaskId::new("override");
717 let crate::streaming::QueueLease::Created { writer, reader, .. } =
718 manager.lease(&task_id, false, Some(1)).await
719 else {
720 panic!("first lease must create a queue");
721 };
722 let mut reader = reader;
723
724 writer
725 .write(make_status_event("override", TaskState::Working))
726 .await
727 .expect("first write");
728 writer
729 .write(make_status_event("override", TaskState::Completed))
730 .await
731 .expect("second write");
732
733 assert!(
734 reader.read().await.expect("reader is still open").is_err(),
735 "the override asked for capacity 1; buffering both events is what \
736 the manager's own default (256) would do"
737 );
738 }
739
740 #[tokio::test]
743 async fn lease_without_an_override_uses_the_managers_capacity() {
744 let manager = EventQueueManager::with_capacity(1);
745 let task_id = TaskId::new("no-override");
746 let crate::streaming::QueueLease::Created { writer, reader, .. } =
747 manager.lease(&task_id, false, None).await
748 else {
749 panic!("first lease must create a queue");
750 };
751 let mut reader = reader;
752
753 writer
754 .write(make_status_event("no-override", TaskState::Working))
755 .await
756 .expect("first write");
757 writer
758 .write(make_status_event("no-override", TaskState::Completed))
759 .await
760 .expect("second write");
761
762 assert!(
763 reader.read().await.expect("reader is still open").is_err(),
764 "None must fall back to the manager's capacity of 1, not to the default"
765 );
766 }
767
768 #[tokio::test]
778 async fn first_queue_is_tracked_when_a_concurrency_limit_is_set() {
779 let manager = EventQueueManager::new().with_max_concurrent_queues(1);
780
781 let first = TaskId::new("q1");
782 let (_w, reader, persistence) = manager.get_or_create_with_persistence(&first).await;
783 assert!(
784 reader.is_some(),
785 "the first queue is below the limit and must be tracked, \
786 with a reader; None means the at-capacity branch was taken"
787 );
788 assert!(
789 persistence.is_some(),
790 "a tracked queue gets a persistence rx"
791 );
792 assert_eq!(
793 manager.active_count().await,
794 1,
795 "first queue must be stored"
796 );
797
798 let second = TaskId::new("q2");
800 let (_w2, reader2, _p2) = manager.get_or_create_with_persistence(&second).await;
801 assert!(
802 reader2.is_none(),
803 "the second queue exceeds the limit and must not be tracked"
804 );
805 assert_eq!(
806 manager.active_count().await,
807 1,
808 "an over-limit queue must not be stored"
809 );
810 }
811
812 #[tokio::test]
815 async fn raw_subscribe_returns_a_receiver_for_a_live_queue() {
816 let manager = EventQueueManager::new();
817 let task_id = TaskId::new("raw");
818 let (_writer, _reader) = manager.get_or_create(&task_id).await;
819
820 assert!(
821 manager.raw_subscribe(&task_id).await.is_some(),
822 "a live queue must yield a receiver"
823 );
824 assert!(
825 manager
826 .raw_subscribe(&TaskId::new("absent"))
827 .await
828 .is_none(),
829 "an unknown task must yield None — pins that Some is not blanket"
830 );
831 }
832
833 #[tokio::test]
840 async fn subscribe_with_snapshot_returns_a_reader_that_yields_the_snapshot() {
841 let manager = EventQueueManager::new();
842 let task_id = TaskId::new("snap");
843 let (_writer, _reader) = manager.get_or_create(&task_id).await;
844
845 let snapshot = make_status_event("snap", TaskState::Working);
846 let reader = manager.subscribe_with_snapshot(&task_id, snapshot).await;
847 let mut reader = reader.expect("a live queue must yield a reader");
848
849 let first = reader
850 .read()
851 .await
852 .expect("reader is open")
853 .expect("snapshot is delivered as Ok");
854 match first.event {
855 StreamResponse::StatusUpdate(ev) => {
856 assert_eq!(
857 ev.status.state,
858 TaskState::Working,
859 "snapshot arrives first"
860 );
861 }
862 other => panic!("expected the snapshot StatusUpdate first, got {other:?}"),
863 }
864
865 assert!(
866 manager
867 .subscribe_with_snapshot(
868 &TaskId::new("absent"),
869 make_status_event("absent", TaskState::Working)
870 )
871 .await
872 .is_none(),
873 "an unknown task must yield None — pins that Some is not blanket"
874 );
875 }
876}