1use std::future::Future;
19use std::pin::Pin;
20use std::sync::Arc;
21
22use a2a_protocol_types::error::{A2aError, A2aResult};
23use a2a_protocol_types::events::StreamResponse;
24use tokio::sync::{broadcast, mpsc};
25
26use super::{EventQueueReader, EventQueueWriter};
27
28struct CountingWriter(usize);
34
35impl std::io::Write for CountingWriter {
36 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
37 self.0 += buf.len();
38 Ok(buf.len())
39 }
40
41 fn flush(&mut self) -> std::io::Result<()> {
42 Ok(())
43 }
44}
45
46#[derive(Debug, Clone)]
58pub struct InMemoryQueueWriter {
59 tx: broadcast::Sender<A2aResult<StreamResponse>>,
60 persistence_tx: Option<mpsc::Sender<A2aResult<StreamResponse>>>,
67 max_event_size: usize,
69 write_timeout: std::time::Duration,
74}
75
76impl InMemoryQueueWriter {
77 pub(super) const fn new(
79 tx: broadcast::Sender<A2aResult<StreamResponse>>,
80 max_event_size: usize,
81 write_timeout: std::time::Duration,
82 ) -> Self {
83 Self {
84 tx,
85 persistence_tx: None,
86 max_event_size,
87 write_timeout,
88 }
89 }
90
91 pub(super) const fn new_with_persistence(
93 tx: broadcast::Sender<A2aResult<StreamResponse>>,
94 persistence_tx: mpsc::Sender<A2aResult<StreamResponse>>,
95 max_event_size: usize,
96 write_timeout: std::time::Duration,
97 ) -> Self {
98 Self {
99 tx,
100 persistence_tx: Some(persistence_tx),
101 max_event_size,
102 write_timeout,
103 }
104 }
105
106 #[must_use]
111 pub fn subscribe(&self) -> InMemoryQueueReader {
112 InMemoryQueueReader::new(self.tx.subscribe())
113 }
114
115 pub(crate) fn raw_subscribe(&self) -> broadcast::Receiver<A2aResult<StreamResponse>> {
120 self.tx.subscribe()
121 }
122}
123
124#[allow(clippy::manual_async_fn)]
125impl EventQueueWriter for InMemoryQueueWriter {
126 fn write<'a>(
127 &'a self,
128 event: StreamResponse,
129 ) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
130 Box::pin(async move {
131 let serialized_size = {
136 let mut counter = CountingWriter(0);
137 serde_json::to_writer(&mut counter, &event)
138 .map_err(|e| A2aError::internal(format!("event serialization failed: {e}")))?;
139 counter.0
140 };
141 if serialized_size > self.max_event_size {
142 return Err(A2aError::internal(format!(
143 "event size {serialized_size} bytes exceeds maximum {} bytes",
144 self.max_event_size
145 )));
146 }
147 if let Some(ref persistence_tx) = self.persistence_tx {
160 match persistence_tx
161 .send_timeout(Ok(event.clone()), self.write_timeout)
162 .await
163 {
164 Ok(()) => {}
165 Err(mpsc::error::SendTimeoutError::Closed(_)) => {
166 trace_warn!("persistence channel closed, event not persisted");
167 }
168 Err(mpsc::error::SendTimeoutError::Timeout(_)) => {
169 trace_warn!(
170 timeout_ms =
171 u64::try_from(self.write_timeout.as_millis()).unwrap_or(u64::MAX),
172 "persistence channel full; background processor is not draining"
173 );
174 return Err(A2aError::internal(format!(
175 "event queue: the persistence channel was still full after {:?}; \
176 the background processor is not draining events",
177 self.write_timeout
178 )));
179 }
180 }
181 }
182 match self.tx.send(Ok(event)) {
191 Ok(_) => Ok(()),
192 Err(_) if self.persistence_tx.is_some() => {
193 trace_warn!("no live event subscribers; event persisted only");
194 Ok(())
195 }
196 Err(_) => Err(A2aError::internal("event queue: no active receivers")),
197 }
198 })
199 }
200
201 fn close<'a>(&'a self) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
202 Box::pin(async move {
203 Ok(())
206 })
207 }
208}
209
210pub struct InMemoryQueueReader {
221 rx: broadcast::Receiver<A2aResult<StreamResponse>>,
222 pending_first: Option<A2aResult<StreamResponse>>,
223 reattach: Option<ReattachFn>,
225 saw_terminal: bool,
229}
230
231#[allow(clippy::missing_fields_in_debug)]
235impl std::fmt::Debug for InMemoryQueueReader {
236 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237 f.debug_struct("InMemoryQueueReader")
238 .field("pending_first", &self.pending_first.is_some())
239 .field("reattach", &self.reattach.is_some())
240 .field("saw_terminal", &self.saw_terminal)
241 .finish()
242 }
243}
244
245#[allow(clippy::large_enum_variant)]
249pub enum Reattached {
250 Channel(broadcast::Receiver<A2aResult<StreamResponse>>),
252 Final(StreamResponse),
257 End,
259}
260
261pub type ReattachFn =
264 Arc<dyn Fn() -> Pin<Box<dyn Future<Output = Reattached> + Send>> + Send + Sync>;
265
266const fn carries_terminal_state(event: &StreamResponse) -> bool {
268 match event {
269 StreamResponse::Task(t) => t.status.state.is_terminal(),
270 StreamResponse::StatusUpdate(u) => u.status.state.is_terminal(),
271 _ => false,
272 }
273}
274
275impl InMemoryQueueReader {
276 pub(crate) fn with_reattach(mut self, reattach: ReattachFn) -> Self {
291 self.reattach = Some(reattach);
292 self
293 }
294
295 pub(crate) const fn new(rx: broadcast::Receiver<A2aResult<StreamResponse>>) -> Self {
297 Self {
298 rx,
299 pending_first: None,
300 reattach: None,
301 saw_terminal: false,
302 }
303 }
304
305 pub fn set_first_event(&mut self, event: StreamResponse) {
307 self.pending_first = Some(Ok(event));
308 }
309
310 pub(crate) const fn with_first_event(
312 rx: broadcast::Receiver<A2aResult<StreamResponse>>,
313 first: StreamResponse,
314 ) -> Self {
315 Self {
316 rx,
317 pending_first: Some(Ok(first)),
318 reattach: None,
319 saw_terminal: false,
320 }
321 }
322
323 pub(crate) fn snapshot_then_end(first: StreamResponse) -> Self {
330 let (tx, rx) = broadcast::channel(1);
333 drop(tx);
334 Self {
335 rx,
336 pending_first: Some(Ok(first)),
337 reattach: None,
338 saw_terminal: false,
339 }
340 }
341}
342
343fn lag_error(dropped: u64) -> a2a_protocol_types::error::A2aError {
359 a2a_protocol_types::error::A2aError::stream_lagged(dropped)
360}
361
362#[allow(clippy::redundant_pub_crate)] pub(crate) fn is_lag_error(err: &a2a_protocol_types::error::A2aError) -> bool {
366 err.is_stream_lagged()
367}
368
369impl EventQueueReader for InMemoryQueueReader {
370 fn read(
371 &mut self,
372 ) -> Pin<Box<dyn Future<Output = Option<A2aResult<StreamResponse>>> + Send + '_>> {
373 Box::pin(async move {
374 if let Some(first) = self.pending_first.take() {
377 if let Ok(ref ev) = first {
378 self.saw_terminal |= carries_terminal_state(ev);
379 }
380 return Some(first);
381 }
382 loop {
383 match self.rx.recv().await {
384 Ok(event) => {
385 if let Ok(ref ev) = event {
386 self.saw_terminal |= carries_terminal_state(ev);
387 }
388 return Some(event);
389 }
390 Err(broadcast::error::RecvError::Lagged(n)) => {
391 trace_warn!(
392 dropped_events = n,
393 "event queue reader lagged, {n} events dropped"
394 );
395 return Some(Err(lag_error(n)));
396 }
397 Err(broadcast::error::RecvError::Closed) => {
398 if self.saw_terminal {
403 return None;
404 }
405 let reattach = self.reattach.as_ref()?;
406 match reattach().await {
407 Reattached::Channel(rx) => self.rx = rx,
408 Reattached::Final(event) => {
409 self.saw_terminal = true;
410 return Some(Ok(event));
411 }
412 Reattached::End => return None,
413 }
414 }
415 }
416 }
417 })
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424 use crate::streaming::event_queue::{
425 new_in_memory_queue, new_in_memory_queue_with_options,
426 new_in_memory_queue_with_persistence, DEFAULT_MAX_EVENT_SIZE, DEFAULT_WRITE_TIMEOUT,
427 };
428 use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
429 use a2a_protocol_types::task::{ContextId, TaskId, TaskState, TaskStatus};
430
431 fn make_status_event(task_id: &str, state: TaskState) -> StreamResponse {
433 StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
434 task_id: TaskId::new(task_id),
435 context_id: ContextId::new("ctx-test"),
436 status: TaskStatus {
437 state,
438 message: None,
439 timestamp: None,
440 },
441 metadata: None,
442 })
443 }
444
445 fn counting_reattach(flag: &Arc<std::sync::atomic::AtomicBool>) -> ReattachFn {
457 let flag = Arc::clone(flag);
458 Arc::new(move || {
459 let flag = Arc::clone(&flag);
460 Box::pin(async move {
461 flag.store(true, std::sync::atomic::Ordering::SeqCst);
462 Reattached::End
463 })
464 })
465 }
466
467 #[tokio::test]
472 async fn terminal_status_update_ends_the_stream_without_reattaching() {
473 use std::sync::atomic::{AtomicBool, Ordering};
474
475 let (writer, reader) = new_in_memory_queue();
476 let called = Arc::new(AtomicBool::new(false));
477 let mut reader = reader.with_reattach(counting_reattach(&called));
478
479 writer
480 .write(make_status_event("t-term", TaskState::Completed))
481 .await
482 .unwrap();
483 drop(writer);
484
485 assert!(reader.read().await.is_some(), "the terminal frame arrives");
486 assert!(
487 reader.read().await.is_none(),
488 "a stream that delivered a terminal state ends at close"
489 );
490 assert!(
491 !called.load(Ordering::SeqCst),
492 "the reattach hook must not be consulted once a terminal state has been seen"
493 );
494 }
495
496 #[tokio::test]
500 async fn terminal_task_snapshot_ends_the_stream_without_reattaching() {
501 use a2a_protocol_types::task::Task;
502 use std::sync::atomic::{AtomicBool, Ordering};
503
504 let (writer, reader) = new_in_memory_queue();
505 let called = Arc::new(AtomicBool::new(false));
506 let mut reader = reader.with_reattach(counting_reattach(&called));
507 reader.set_first_event(StreamResponse::Task(Task {
508 id: TaskId::new("t-snap"),
509 context_id: ContextId::new("ctx-test"),
510 status: TaskStatus {
511 state: TaskState::Completed,
512 message: None,
513 timestamp: None,
514 },
515 history: None,
516 artifacts: None,
517 metadata: None,
518 }));
519 drop(writer);
520
521 assert!(reader.read().await.is_some(), "the snapshot arrives first");
522 assert!(
523 reader.read().await.is_none(),
524 "a terminal Task snapshot ends the stream at close"
525 );
526 assert!(
527 !called.load(Ordering::SeqCst),
528 "the reattach hook must not be consulted after a terminal Task snapshot"
529 );
530 }
531
532 #[tokio::test]
537 async fn non_terminal_event_still_consults_the_reattach_hook() {
538 use std::sync::atomic::{AtomicBool, Ordering};
539
540 let (writer, reader) = new_in_memory_queue();
541 let called = Arc::new(AtomicBool::new(false));
542 let mut reader = reader.with_reattach(counting_reattach(&called));
543
544 writer
545 .write(make_status_event("t-working", TaskState::Working))
546 .await
547 .unwrap();
548 drop(writer);
549
550 assert!(reader.read().await.is_some(), "the working frame arrives");
551 assert!(reader.read().await.is_none(), "the hook here returns End");
552 assert!(
553 called.load(Ordering::SeqCst),
554 "without a terminal state the reader must ask the hook whether the task is done"
555 );
556 }
557
558 #[tokio::test]
564 async fn event_of_exactly_max_size_is_accepted() {
565 let event = make_status_event("t-exact", TaskState::Working);
566 let exact = serde_json::to_vec(&event).expect("serializes").len();
567
568 let (writer, _reader) = new_in_memory_queue_with_options(16, exact, DEFAULT_WRITE_TIMEOUT);
569 assert!(
570 writer.write(event).await.is_ok(),
571 "an event of exactly max_event_size ({exact} bytes) is within the \
572 limit and must be accepted"
573 );
574
575 let event = make_status_event("t-exact", TaskState::Working);
578 let (writer, _reader) =
579 new_in_memory_queue_with_options(16, exact - 1, DEFAULT_WRITE_TIMEOUT);
580 assert!(
581 writer.write(event).await.is_err(),
582 "one byte over the limit must still be rejected"
583 );
584 }
585
586 #[tokio::test]
591 async fn reader_debug_reports_the_decision_carrying_fields() {
592 let (_writer, mut reader) = new_in_memory_queue();
593 reader.set_first_event(make_status_event("t-dbg", TaskState::Working));
594
595 let rendered = format!("{reader:?}");
596 assert!(
597 rendered.contains("InMemoryQueueReader"),
598 "the type name must appear: {rendered}"
599 );
600 assert!(
601 rendered.contains("pending_first: true"),
602 "a pending snapshot must be visible: {rendered}"
603 );
604 assert!(
605 rendered.contains("saw_terminal: false"),
606 "terminal tracking must be visible: {rendered}"
607 );
608 }
609
610 #[tokio::test]
618 async fn write_with_no_subscribers_succeeds_when_persistence_attached() {
619 let (writer, reader, mut persistence_rx) =
620 crate::streaming::event_queue::new_in_memory_queue_with_persistence(
621 8,
622 1024 * 1024,
623 std::time::Duration::from_secs(1),
624 );
625 drop(reader); writer
628 .write(make_status_event("t1", TaskState::Working))
629 .await
630 .expect("write must succeed with persistence attached");
631
632 let persisted = persistence_rx
633 .recv()
634 .await
635 .expect("persistence channel should have the event")
636 .expect("event should be Ok");
637 match persisted {
638 StreamResponse::StatusUpdate(evt) => {
639 assert_eq!(evt.status.state, TaskState::Working);
640 }
641 other => panic!("expected StatusUpdate, got: {other:?}"),
642 }
643 }
644
645 #[tokio::test]
649 async fn write_with_no_subscribers_fails_without_persistence() {
650 let (writer, reader) = new_in_memory_queue();
651 drop(reader);
652
653 let result = writer
654 .write(make_status_event("t1", TaskState::Working))
655 .await;
656 assert!(
657 result.is_err(),
658 "sync-mode write with no receivers must fail"
659 );
660 }
661
662 #[tokio::test]
663 async fn write_then_read_single_event() {
664 let (writer, mut reader) = new_in_memory_queue();
665 let event = make_status_event("t1", TaskState::Working);
666
667 writer.write(event).await.expect("write should succeed");
668 drop(writer);
669
670 let received = reader.read().await;
671 assert!(received.is_some(), "reader should return the written event");
672 let result = received.unwrap();
673 let event = result.expect("event should be Ok");
674 match &event {
675 StreamResponse::StatusUpdate(evt) => {
676 assert_eq!(
677 evt.status.state,
678 TaskState::Working,
679 "should be Working event"
680 );
681 }
682 other => panic!("expected StatusUpdate, got: {other:?}"),
683 }
684
685 let eof = reader.read().await;
687 assert!(
688 eof.is_none(),
689 "reader should return None after writer is dropped"
690 );
691 }
692
693 #[tokio::test]
694 async fn write_multiple_events_read_in_order() {
695 let (writer, mut reader) = new_in_memory_queue();
696
697 let e1 = make_status_event("t1", TaskState::Working);
698 let e2 = make_status_event("t1", TaskState::Completed);
699
700 writer.write(e1).await.expect("first write should succeed");
701 writer.write(e2).await.expect("second write should succeed");
702 drop(writer);
703
704 let r1 = reader.read().await.expect("should read first event");
706 let sr1 = r1.expect("first event should be Ok");
707 match &sr1 {
708 StreamResponse::StatusUpdate(evt) => {
709 assert_eq!(
710 evt.status.state,
711 TaskState::Working,
712 "first event should be Working"
713 );
714 }
715 other => panic!("expected StatusUpdate, got: {other:?}"),
716 }
717
718 let r2 = reader.read().await.expect("should read second event");
720 let sr2 = r2.expect("second event should be Ok");
721 match &sr2 {
722 StreamResponse::StatusUpdate(evt) => {
723 assert_eq!(
724 evt.status.state,
725 TaskState::Completed,
726 "second event should be Completed"
727 );
728 }
729 other => panic!("expected StatusUpdate, got: {other:?}"),
730 }
731
732 assert!(
734 reader.read().await.is_none(),
735 "should be EOF after all events"
736 );
737 }
738
739 #[tokio::test]
742 async fn read_returns_none_on_empty_closed_queue() {
743 let (writer, mut reader) = new_in_memory_queue();
744 drop(writer); let result = reader.read().await;
747 assert!(
748 result.is_none(),
749 "reading from an empty closed queue should return None"
750 );
751 }
752
753 #[tokio::test]
754 async fn write_after_all_readers_dropped_returns_error() {
755 let (writer, reader) = new_in_memory_queue();
756 drop(reader);
757
758 let result = writer
759 .write(make_status_event("t1", TaskState::Working))
760 .await;
761 assert!(
762 result.is_err(),
763 "writing with no active receivers should return an error"
764 );
765 }
766
767 #[tokio::test]
768 async fn close_is_no_op_and_succeeds() {
769 let (writer, _reader) = new_in_memory_queue();
770 let result = writer.close().await;
771 assert!(result.is_ok(), "close() should succeed");
772 }
773
774 #[tokio::test]
777 async fn subscribe_creates_independent_reader() {
778 let (writer, mut reader1) = new_in_memory_queue();
779 let mut reader2 = writer.subscribe();
780
781 let event = make_status_event("t1", TaskState::Working);
782 writer.write(event).await.expect("write should succeed");
783 drop(writer);
784
785 let r1 = reader1.read().await;
787 assert!(r1.is_some(), "reader1 should receive the event");
788
789 let r2 = reader2.read().await;
790 assert!(r2.is_some(), "reader2 should receive the event");
791
792 assert!(reader1.read().await.is_none(), "reader1 should see EOF");
794 assert!(reader2.read().await.is_none(), "reader2 should see EOF");
795 }
796
797 #[tokio::test]
798 async fn subscriber_only_sees_events_after_subscribe() {
799 let (writer, mut reader1) = new_in_memory_queue();
800
801 writer
803 .write(make_status_event("t1", TaskState::Submitted))
804 .await
805 .expect("write should succeed");
806
807 let mut reader2 = writer.subscribe();
809
810 writer
812 .write(make_status_event("t1", TaskState::Working))
813 .await
814 .expect("write should succeed");
815 drop(writer);
816
817 let r1a = reader1
819 .read()
820 .await
821 .expect("reader1 should see first event");
822 let evt1a = r1a.expect("first event should be Ok");
823 assert!(
824 matches!(&evt1a, StreamResponse::StatusUpdate(e) if e.status.state == TaskState::Submitted),
825 "reader1 first event should be Submitted"
826 );
827 let r1b = reader1
828 .read()
829 .await
830 .expect("reader1 should see second event");
831 let evt_1b = r1b.expect("second event should be Ok");
832 assert!(
833 matches!(&evt_1b, StreamResponse::StatusUpdate(e) if e.status.state == TaskState::Working),
834 "reader1 second event should be Working"
835 );
836 assert!(reader1.read().await.is_none());
837
838 let r2a = reader2
840 .read()
841 .await
842 .expect("reader2 should see second event");
843 let evt2a = r2a.expect("event should be Ok");
844 assert!(
845 matches!(&evt2a, StreamResponse::StatusUpdate(e) if e.status.state == TaskState::Working),
846 "reader2 should see Working event"
847 );
848 assert!(
849 reader2.read().await.is_none(),
850 "reader2 should see EOF after the one event it received"
851 );
852 }
853
854 #[tokio::test]
857 async fn oversized_event_is_rejected() {
858 let (writer, _reader) = new_in_memory_queue_with_options(
860 16,
861 10, DEFAULT_WRITE_TIMEOUT,
863 );
864
865 let event = make_status_event("t1", TaskState::Working);
866 let result = writer.write(event).await;
867 assert!(
868 result.is_err(),
869 "event exceeding max_event_size should be rejected"
870 );
871 let err = result.unwrap_err();
872 let msg = format!("{err}");
873 assert!(
874 msg.contains("exceeds maximum"),
875 "error message should mention size limit, got: {msg}"
876 );
877 }
878
879 #[test]
881 fn counting_writer_flush_is_noop() {
882 use std::io::Write;
883 let mut cw = super::CountingWriter(0);
884 cw.write_all(b"hello").unwrap();
885 assert_eq!(cw.0, 5);
886 cw.flush().unwrap();
888 assert_eq!(cw.0, 5, "flush should not change the count");
889 }
890
891 #[tokio::test]
892 async fn event_within_size_limit_is_accepted() {
893 let (writer, mut reader) =
895 new_in_memory_queue_with_options(16, DEFAULT_MAX_EVENT_SIZE, DEFAULT_WRITE_TIMEOUT);
896
897 let event = make_status_event("t1", TaskState::Working);
898 writer
899 .write(event)
900 .await
901 .expect("event within size limit should be accepted");
902 drop(writer);
903
904 let r = reader.read().await;
905 assert!(r.is_some(), "reader should receive the event");
906 }
907
908 #[tokio::test]
921 async fn a_full_persistence_channel_fails_the_write_within_the_timeout() {
922 let timeout = std::time::Duration::from_millis(150);
923 let (writer, _sse, _held_receiver) =
924 new_in_memory_queue_with_persistence(1, DEFAULT_MAX_EVENT_SIZE, timeout);
925
926 let mut accepted = 0_usize;
932 let start = std::time::Instant::now();
933 loop {
934 let attempt = tokio::time::timeout(
935 timeout * 10,
936 writer.write(make_status_event("t1", TaskState::Working)),
937 )
938 .await
939 .expect("write parked past its own deadline: write_timeout is not being applied");
940 match attempt {
941 Ok(()) => accepted += 1,
942 Err(e) => {
943 assert!(
944 e.to_string().contains("persistence channel"),
945 "the error must name the channel that is full, got: {e}"
946 );
947 break;
948 }
949 }
950 assert!(
951 accepted <= 4096,
952 "write never reported a full channel after {accepted} events"
953 );
954 }
955 assert_eq!(
956 accepted, 1024,
957 "the persistence channel's capacity is max(capacity * 16, 1024)"
958 );
959 assert!(
962 start.elapsed() < timeout * 10,
963 "write should fail after roughly one timeout, took {:?}",
964 start.elapsed()
965 );
966 }
967
968 #[tokio::test]
971 async fn the_failing_write_waits_about_one_write_timeout() {
972 let timeout = std::time::Duration::from_millis(200);
973 let (writer, _sse, _held_receiver) =
974 new_in_memory_queue_with_persistence(1, DEFAULT_MAX_EVENT_SIZE, timeout);
975 for _ in 0..1024 {
976 writer
977 .write(make_status_event("t1", TaskState::Working))
978 .await
979 .expect("the channel has room until it is full");
980 }
981
982 let start = std::time::Instant::now();
983 let err = tokio::time::timeout(
984 timeout * 10,
985 writer.write(make_status_event("t1", TaskState::Working)),
986 )
987 .await
988 .expect("write parked past its own deadline: write_timeout is not being applied")
989 .expect_err("the channel is full");
990 let waited = start.elapsed();
991
992 assert!(waited >= timeout, "returned early after {waited:?}");
993 assert!(
994 waited < timeout * 4,
995 "waited far past the deadline: {waited:?}"
996 );
997 assert!(
998 err.to_string().contains("not draining"),
999 "the message must say what is wrong, got: {err}"
1000 );
1001 }
1002
1003 #[tokio::test]
1007 async fn a_closed_persistence_channel_is_still_not_an_error() {
1008 let (writer, _sse, persistence_rx) = new_in_memory_queue_with_persistence(
1009 16,
1010 DEFAULT_MAX_EVENT_SIZE,
1011 std::time::Duration::from_millis(50),
1012 );
1013 drop(persistence_rx);
1014
1015 let start = std::time::Instant::now();
1016 tokio::time::timeout(
1017 std::time::Duration::from_secs(5),
1018 writer.write(make_status_event("t1", TaskState::Working)),
1019 )
1020 .await
1021 .expect("a closed channel must be observed at once, never waited out")
1022 .expect("a closed persistence channel must not fail the write");
1023 assert!(
1024 start.elapsed() < std::time::Duration::from_millis(50),
1025 "a closed channel must be detected immediately, not waited out"
1026 );
1027 }
1028}