a2a_protocol_server/streaming/event_queue/
manager.rs1use 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 new_in_memory_queue_with_options, new_in_memory_queue_with_persistence, InMemoryQueueReader,
19 InMemoryQueueWriter, DEFAULT_MAX_EVENT_SIZE, DEFAULT_QUEUE_CAPACITY, DEFAULT_WRITE_TIMEOUT,
20};
21use crate::metrics::Metrics;
22
23#[allow(clippy::large_enum_variant)]
38pub enum QueueLease {
39 Created {
42 writer: Arc<InMemoryQueueWriter>,
43 reader: InMemoryQueueReader,
44 persistence_rx: Option<tokio::sync::mpsc::Receiver<A2aResult<StreamResponse>>>,
45 },
46 Existing,
51 CapacityExhausted,
54}
55
56#[derive(Clone)]
64pub struct EventQueueManager {
65 writers: Arc<RwLock<HashMap<TaskId, Arc<InMemoryQueueWriter>>>>,
66 capacity: usize,
68 max_event_size: usize,
70 write_timeout: std::time::Duration,
72 max_concurrent_queues: Option<usize>,
74 metrics: Option<Arc<dyn Metrics>>,
76}
77
78impl std::fmt::Debug for EventQueueManager {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 f.debug_struct("EventQueueManager")
81 .field("writers", &"<RwLock<HashMap<...>>>")
82 .field("capacity", &self.capacity)
83 .field("max_event_size", &self.max_event_size)
84 .field("write_timeout", &self.write_timeout)
85 .field("max_concurrent_queues", &self.max_concurrent_queues)
86 .field("metrics", &self.metrics.is_some())
87 .finish()
88 }
89}
90
91impl Default for EventQueueManager {
92 fn default() -> Self {
93 Self {
94 writers: Arc::default(),
95 capacity: DEFAULT_QUEUE_CAPACITY,
96 max_event_size: DEFAULT_MAX_EVENT_SIZE,
97 write_timeout: DEFAULT_WRITE_TIMEOUT,
98 max_concurrent_queues: None,
99 metrics: None,
100 }
101 }
102}
103
104impl EventQueueManager {
105 #[must_use]
115 pub fn new() -> Self {
116 Self::default()
117 }
118
119 #[must_use]
121 pub fn with_capacity(capacity: usize) -> Self {
122 Self {
123 writers: Arc::default(),
124 capacity,
125 max_event_size: DEFAULT_MAX_EVENT_SIZE,
126 write_timeout: DEFAULT_WRITE_TIMEOUT,
127 max_concurrent_queues: None,
128 metrics: None,
129 }
130 }
131
132 #[deprecated(
139 since = "0.7.0",
140 note = "has no effect: broadcast-based queues never block on writes; \
141 slow consumers receive an explicit lag error instead. \
142 Will be removed in 0.8."
143 )]
144 #[must_use]
145 pub const fn with_write_timeout(mut self, timeout: std::time::Duration) -> Self {
146 self.write_timeout = timeout;
147 self
148 }
149
150 #[must_use]
155 pub const fn with_max_event_size(mut self, max_event_size: usize) -> Self {
156 self.max_event_size = max_event_size;
157 self
158 }
159
160 #[must_use]
162 pub fn with_metrics(mut self, metrics: Arc<dyn Metrics>) -> Self {
163 self.metrics = Some(metrics);
164 self
165 }
166
167 #[must_use]
172 pub const fn with_max_concurrent_queues(mut self, max: usize) -> Self {
173 self.max_concurrent_queues = Some(max);
174 self
175 }
176
177 pub async fn get_or_create(
188 &self,
189 task_id: &TaskId,
190 ) -> (Arc<InMemoryQueueWriter>, Option<InMemoryQueueReader>) {
191 let mut map = self.writers.write().await;
192 #[allow(clippy::option_if_let_else)]
193 let result = if let Some(existing) = map.get(task_id) {
194 (Arc::clone(existing), None)
195 } else if self
196 .max_concurrent_queues
197 .is_some_and(|max| map.len() >= max)
198 {
199 let (writer, _reader) = new_in_memory_queue_with_options(
202 self.capacity,
203 self.max_event_size,
204 self.write_timeout,
205 );
206 (Arc::new(writer), None)
207 } else {
208 let (writer, reader) = new_in_memory_queue_with_options(
209 self.capacity,
210 self.max_event_size,
211 self.write_timeout,
212 );
213 let writer = Arc::new(writer);
214 map.insert(task_id.clone(), Arc::clone(&writer));
215 (writer, Some(reader))
216 };
217 let queue_count = map.len();
218 drop(map);
219 if let Some(ref metrics) = self.metrics {
220 metrics.on_queue_depth_change(queue_count);
221 }
222 result
223 }
224
225 pub async fn get_or_create_with_persistence(
233 &self,
234 task_id: &TaskId,
235 ) -> (
236 Arc<InMemoryQueueWriter>,
237 Option<InMemoryQueueReader>,
238 Option<tokio::sync::mpsc::Receiver<A2aResult<StreamResponse>>>,
239 ) {
240 let mut map = self.writers.write().await;
241 #[allow(clippy::option_if_let_else)]
242 let result = if let Some(existing) = map.get(task_id) {
243 (Arc::clone(existing), None, None)
244 } else if self
245 .max_concurrent_queues
246 .is_some_and(|max| map.len() >= max)
247 {
248 let (writer, _reader) = new_in_memory_queue_with_options(
249 self.capacity,
250 self.max_event_size,
251 self.write_timeout,
252 );
253 (Arc::new(writer), None, None)
254 } else {
255 let (writer, reader, persistence_rx) = new_in_memory_queue_with_persistence(
256 self.capacity,
257 self.max_event_size,
258 self.write_timeout,
259 );
260 let writer = Arc::new(writer);
261 map.insert(task_id.clone(), Arc::clone(&writer));
262 (writer, Some(reader), Some(persistence_rx))
263 };
264 let queue_count = map.len();
265 drop(map);
266 if let Some(ref metrics) = self.metrics {
267 metrics.on_queue_depth_change(queue_count);
268 }
269 result
270 }
271
272 #[allow(clippy::option_if_let_else)]
284 pub(crate) async fn lease(&self, task_id: &TaskId, with_persistence: bool) -> QueueLease {
285 let mut map = self.writers.write().await;
286 let lease = if map.contains_key(task_id) {
287 QueueLease::Existing
288 } else if self
289 .max_concurrent_queues
290 .is_some_and(|max| map.len() >= max)
291 {
292 QueueLease::CapacityExhausted
293 } else if with_persistence {
294 let (writer, reader, persistence_rx) = new_in_memory_queue_with_persistence(
295 self.capacity,
296 self.max_event_size,
297 self.write_timeout,
298 );
299 let writer = Arc::new(writer);
300 map.insert(task_id.clone(), Arc::clone(&writer));
301 QueueLease::Created {
302 writer,
303 reader,
304 persistence_rx: Some(persistence_rx),
305 }
306 } else {
307 let (writer, reader) = new_in_memory_queue_with_options(
308 self.capacity,
309 self.max_event_size,
310 self.write_timeout,
311 );
312 let writer = Arc::new(writer);
313 map.insert(task_id.clone(), Arc::clone(&writer));
314 QueueLease::Created {
315 writer,
316 reader,
317 persistence_rx: None,
318 }
319 };
320 let queue_count = map.len();
321 drop(map);
322 if let Some(ref metrics) = self.metrics {
323 metrics.on_queue_depth_change(queue_count);
324 }
325 lease
326 }
327
328 pub(crate) async fn writer_for_cancel(&self, task_id: &TaskId) -> Arc<InMemoryQueueWriter> {
338 {
339 let map = self.writers.read().await;
340 if let Some(writer) = map.get(task_id) {
341 return Arc::clone(writer);
342 }
343 }
344 let (writer, _reader) = new_in_memory_queue_with_options(
345 self.capacity,
346 self.max_event_size,
347 self.write_timeout,
348 );
349 Arc::new(writer)
350 }
351
352 pub async fn subscribe(&self, task_id: &TaskId) -> Option<InMemoryQueueReader> {
360 let map = self.writers.read().await;
361 map.get(task_id).map(|writer| writer.subscribe())
362 }
363
364 pub async fn subscribe_with_snapshot(
373 &self,
374 task_id: &TaskId,
375 snapshot: StreamResponse,
376 ) -> Option<InMemoryQueueReader> {
377 let map = self.writers.read().await;
378 let writer = map.get(task_id)?;
379 let rx = writer.raw_subscribe();
383 drop(map);
384 Some(InMemoryQueueReader::with_first_event(rx, snapshot))
385 }
386
387 pub async fn destroy(&self, task_id: &TaskId) {
389 let mut map = self.writers.write().await;
390 map.remove(task_id);
391 let queue_count = map.len();
392 drop(map);
393 if let Some(ref metrics) = self.metrics {
394 metrics.on_queue_depth_change(queue_count);
395 }
396 }
397
398 pub async fn active_count(&self) -> usize {
400 let map = self.writers.read().await;
401 map.len()
402 }
403
404 pub(crate) async fn has_queue(&self, task_id: &TaskId) -> bool {
410 self.writers.read().await.contains_key(task_id)
411 }
412
413 #[must_use]
416 pub(crate) const fn max_concurrent_queues(&self) -> Option<usize> {
417 self.max_concurrent_queues
418 }
419
420 pub async fn destroy_all(&self) {
422 let mut map = self.writers.write().await;
423 map.clear();
424 }
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430 use crate::streaming::event_queue::EventQueueWriter;
431 use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
432 use a2a_protocol_types::task::{ContextId, TaskState, TaskStatus};
433
434 fn make_status_event(task_id: &str, state: TaskState) -> StreamResponse {
436 StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
437 task_id: TaskId::new(task_id),
438 context_id: ContextId::new("ctx-test"),
439 status: TaskStatus {
440 state,
441 message: None,
442 timestamp: None,
443 },
444 metadata: None,
445 })
446 }
447
448 #[test]
451 fn max_concurrent_queues_reports_configured_limit() {
452 assert_eq!(EventQueueManager::new().max_concurrent_queues(), None);
454 assert_eq!(
456 EventQueueManager::new()
457 .with_max_concurrent_queues(42)
458 .max_concurrent_queues(),
459 Some(42)
460 );
461 }
462
463 #[tokio::test]
464 async fn manager_get_or_create_new_task() {
465 let manager = EventQueueManager::new();
466 let task_id = TaskId::new("task-1");
467
468 let (writer, reader) = manager.get_or_create(&task_id).await;
469 assert!(
470 reader.is_some(),
471 "first get_or_create should return a reader"
472 );
473
474 writer
476 .write(make_status_event("task-1", TaskState::Working))
477 .await
478 .expect("write through manager writer should succeed");
479
480 assert_eq!(
481 manager.active_count().await,
482 1,
483 "should have 1 active queue"
484 );
485 }
486
487 #[tokio::test]
488 async fn manager_get_or_create_existing_task_returns_no_reader() {
489 let manager = EventQueueManager::new();
490 let task_id = TaskId::new("task-1");
491
492 let (_w1, r1) = manager.get_or_create(&task_id).await;
493 assert!(r1.is_some(), "first call should return a reader");
494
495 let (_w2, r2) = manager.get_or_create(&task_id).await;
496 assert!(
497 r2.is_none(),
498 "second call for same task should return None reader"
499 );
500
501 assert_eq!(
502 manager.active_count().await,
503 1,
504 "should still have only 1 active queue"
505 );
506 }
507
508 #[tokio::test]
509 async fn manager_subscribe_existing_task() {
510 use crate::streaming::event_queue::EventQueueReader;
511
512 let manager = EventQueueManager::new();
513 let task_id = TaskId::new("task-1");
514
515 let (writer, _reader) = manager.get_or_create(&task_id).await;
516
517 let sub = manager.subscribe(&task_id).await;
518 assert!(
519 sub.is_some(),
520 "subscribe should return a reader for existing task"
521 );
522
523 let mut sub_reader = sub.unwrap();
524 writer
525 .write(make_status_event("task-1", TaskState::Working))
526 .await
527 .expect("write should succeed");
528 drop(writer);
529
530 let r = sub_reader.read().await;
531 assert!(r.is_some(), "subscriber should receive the event");
532 }
533
534 #[tokio::test]
535 async fn manager_subscribe_nonexistent_task_returns_none() {
536 let manager = EventQueueManager::new();
537 let task_id = TaskId::new("no-such-task");
538
539 let sub = manager.subscribe(&task_id).await;
540 assert!(
541 sub.is_none(),
542 "subscribe should return None for nonexistent task"
543 );
544 }
545
546 #[tokio::test]
547 async fn manager_destroy_removes_queue() {
548 let manager = EventQueueManager::new();
549 let task_id = TaskId::new("task-1");
550
551 let (_writer, _reader) = manager.get_or_create(&task_id).await;
552 assert_eq!(manager.active_count().await, 1);
553
554 manager.destroy(&task_id).await;
555 assert_eq!(
556 manager.active_count().await,
557 0,
558 "destroy should remove the queue"
559 );
560 }
561
562 #[tokio::test]
563 async fn manager_destroy_all_clears_queues() {
564 let manager = EventQueueManager::new();
565
566 let _q1 = manager.get_or_create(&TaskId::new("t1")).await;
567 let _q2 = manager.get_or_create(&TaskId::new("t2")).await;
568 assert_eq!(manager.active_count().await, 2);
569
570 manager.destroy_all().await;
571 assert_eq!(
572 manager.active_count().await,
573 0,
574 "destroy_all should clear all queues"
575 );
576 }
577
578 #[tokio::test]
579 async fn lease_reports_existing_and_has_queue() {
580 let manager = EventQueueManager::new();
581 let task = TaskId::new("t-lease");
582
583 assert!(matches!(
585 manager.lease(&task, true).await,
586 QueueLease::Created { .. }
587 ));
588 assert!(manager.has_queue(&task).await, "queue should now be live");
589
590 assert!(matches!(
593 manager.lease(&task, true).await,
594 QueueLease::Existing
595 ));
596
597 assert!(!manager.has_queue(&TaskId::new("other")).await);
599 }
600
601 #[tokio::test]
602 async fn manager_max_concurrent_queues_enforced() {
603 let manager = EventQueueManager::new().with_max_concurrent_queues(1);
604
605 let (_w1, r1) = manager.get_or_create(&TaskId::new("t1")).await;
606 assert!(r1.is_some(), "first queue should be created successfully");
607
608 let (_w2, r2) = manager.get_or_create(&TaskId::new("t2")).await;
610 assert!(
611 r2.is_none(),
612 "second queue should return None reader when limit is reached"
613 );
614 assert_eq!(
615 manager.active_count().await,
616 1,
617 "should still have only 1 queue (second was not stored)"
618 );
619 }
620
621 #[tokio::test]
623 #[allow(deprecated)] async fn manager_with_write_timeout() {
625 let manager =
626 EventQueueManager::new().with_write_timeout(std::time::Duration::from_secs(10));
627 let task_id = TaskId::new("t1");
629 let (writer, reader) = manager.get_or_create(&task_id).await;
630 assert!(reader.is_some());
631 writer
632 .write(make_status_event("t1", TaskState::Working))
633 .await
634 .expect("write should succeed with custom write_timeout");
635 }
636
637 #[tokio::test]
638 async fn manager_with_capacity_and_max_event_size() {
639 let manager = EventQueueManager::with_capacity(4).with_max_event_size(10); let task_id = TaskId::new("t1");
642 let (writer, _reader) = manager.get_or_create(&task_id).await;
643
644 let event = make_status_event("t1", TaskState::Working);
645 let result = writer.write(event).await;
646 assert!(
647 result.is_err(),
648 "event should be rejected by the size limit configured on the manager"
649 );
650 }
651}