1use std::{
2 collections::{HashMap, VecDeque},
3 sync::LazyLock,
4};
5
6use dora_message::{
7 config::{DEFAULT_QUEUE_SIZE, QueuePolicy},
8 daemon_to_node::NodeEvent,
9 id::DataId,
10 metadata::{
11 GOAL_ID, GOAL_STATUS, MetadataParameters, REQUEST_ID, carries_pattern_correlation,
12 get_string_param,
13 },
14};
15
16use super::thread::EventItem;
17
18fn event_parameters(event: &EventItem) -> Option<&MetadataParameters> {
25 match event {
26 EventItem::NodeEvent {
27 event: NodeEvent::Input { metadata, .. },
28 ..
29 } => Some(&metadata.parameters),
30 EventItem::ZenohInput { metadata, .. } => Some(&metadata.parameters),
31 _ => None,
32 }
33}
34
35fn is_correlated(event: &EventItem) -> bool {
43 event_parameters(event).is_some_and(carries_pattern_correlation)
50}
51
52fn is_stop(event: &EventItem) -> bool {
60 matches!(
61 event,
62 EventItem::NodeEvent {
63 event: NodeEvent::Stop,
64 ..
65 }
66 )
67}
68
69enum Eviction {
71 RemoveAt(usize),
73 DropIncoming,
77 DropCorrelatedLoud(usize),
81}
82
83fn select_eviction(queue: &VecDeque<EventItem>, incoming: &EventItem) -> Eviction {
90 if let Some(idx) = queue.iter().position(|e| !is_correlated(e) && !is_stop(e)) {
92 return Eviction::RemoveAt(idx);
93 }
94 if !is_correlated(incoming) && !is_stop(incoming) {
97 return Eviction::DropIncoming;
98 }
99 match queue.iter().position(|e| !is_stop(e)) {
104 Some(idx) => Eviction::DropCorrelatedLoud(idx),
105 None => Eviction::DropIncoming,
106 }
107}
108
109fn log_correlation_drop(event_id: &DataId, dropped: &EventItem) {
113 let Some(params) = event_parameters(dropped) else {
114 return;
115 };
116 log_correlation_drop_params(event_id, params);
117}
118
119pub(crate) fn log_correlation_drop_params(input_id: &DataId, params: &MetadataParameters) {
125 let request_id = get_string_param(params, REQUEST_ID);
126 let goal_id = get_string_param(params, GOAL_ID);
127 let goal_status = get_string_param(params, GOAL_STATUS);
128 tracing::error!(
129 input = %input_id,
130 ?request_id,
131 ?goal_id,
132 ?goal_status,
133 "queue full of correlated messages; dropping oldest correlation. \
134 This breaks the service/action request-response contract. \
135 Consider increasing queue_size or switching this input to \
136 `queue_policy: backpressure`."
137 );
138}
139pub(crate) const NON_INPUT_EVENT: &str = "dora.non_input_event";
140
141pub(crate) const NON_INPUT_EVENT_QUEUE_SIZE: usize = 1_000;
145
146static NON_INPUT_EVENT_ID: LazyLock<DataId> =
149 LazyLock::new(|| DataId::from(NON_INPUT_EVENT.to_string()));
150
151#[derive(Debug)]
223pub struct Scheduler {
224 last_used: VecDeque<DataId>,
226 event_queues: HashMap<DataId, (usize, VecDeque<EventItem>)>,
228 queue_policies: HashMap<DataId, QueuePolicy>,
230 dropped: HashMap<DataId, u64>,
232}
233
234impl Scheduler {
235 pub(crate) fn with_policies(
236 event_queues: HashMap<DataId, (usize, VecDeque<EventItem>)>,
237 queue_policies: HashMap<DataId, QueuePolicy>,
238 ) -> Self {
239 let topic = VecDeque::from_iter(
240 event_queues
241 .keys()
242 .filter(|t| **t != *NON_INPUT_EVENT_ID)
243 .cloned(),
244 );
245 Self {
246 last_used: topic,
247 event_queues,
248 queue_policies,
249 dropped: HashMap::new(),
250 }
251 }
252
253 pub fn drain_drop_counts(&mut self) -> HashMap<DataId, u64> {
255 std::mem::take(&mut self.dropped)
256 }
257
258 pub(crate) fn effective_cap_for(&self, id: &DataId) -> usize {
269 let size = self
270 .event_queues
271 .get(id)
272 .map(|(size, _)| *size)
273 .unwrap_or(DEFAULT_QUEUE_SIZE);
274 let policy = self.queue_policies.get(id).copied().unwrap_or_default();
275 policy.effective_cap(size)
276 }
277
278 pub(crate) fn record_drop(&mut self, id: &DataId) {
283 *self.dropped.entry(id.clone()).or_insert(0) += 1;
284 }
285
286 pub(crate) fn record_non_input_drop(&mut self) {
291 *self.dropped.entry(NON_INPUT_EVENT_ID.clone()).or_insert(0) += 1;
292 }
293
294 pub(crate) fn add_event(&mut self, event: EventItem) {
295 let (event_id, should_flush) = match &event {
296 EventItem::NodeEvent {
297 event: NodeEvent::Input { id, metadata, .. },
298 ..
299 } => {
300 let flush = dora_message::metadata::get_bool_param(
301 &metadata.parameters,
302 dora_message::metadata::FLUSH,
303 ) == Some(true);
304 (id, flush)
305 }
306 EventItem::ZenohInput { id, metadata, .. } => {
307 let flush = dora_message::metadata::get_bool_param(
308 &metadata.parameters,
309 dora_message::metadata::FLUSH,
310 ) == Some(true);
311 (id, flush)
312 }
313 _ => (&*NON_INPUT_EVENT_ID, false),
314 };
315
316 if should_flush && let Some((_size, queue)) = self.event_queues.get_mut(event_id) {
330 let before = queue.len();
331 queue.retain(|e| is_correlated(e) || is_stop(e));
332 let drained = before - queue.len();
333 if drained > 0 {
334 tracing::debug!(
335 "Flushed {drained} queued event(s) for input `{event_id}` (flush signal)"
336 );
337 }
338 if !queue.is_empty() {
339 tracing::debug!(
340 input = %event_id,
341 preserved = queue.len(),
342 "flush signal retained correlated (request_id/goal_id) events"
343 );
344 }
345 }
346
347 if !self.event_queues.contains_key(event_id) {
354 tracing::warn!(
355 "no queue config for input `{event_id}`, using default size {DEFAULT_QUEUE_SIZE}"
356 );
357 self.last_used.push_back(event_id.clone());
358 self.event_queues
359 .insert(event_id.clone(), (DEFAULT_QUEUE_SIZE, Default::default()));
360 }
361 let Some((size, queue)) = self.event_queues.get_mut(event_id) else {
362 return;
364 };
365
366 let policy = self
367 .queue_policies
368 .get(event_id)
369 .copied()
370 .unwrap_or_default();
371
372 let cap = policy.effective_cap(*size);
373 if queue.len() >= cap {
374 if policy == QueuePolicy::Backpressure {
375 tracing::error!(
376 "Backpressure input `{event_id}` hit hard cap ({cap}), \
377 dropping oldest to prevent OOM"
378 );
379 } else {
380 tracing::warn!("Discarding event for input `{event_id}` due to queue size limit");
381 }
382 *self.dropped.entry(event_id.clone()).or_insert(0) += 1;
383 match select_eviction(queue, &event) {
384 Eviction::RemoveAt(idx) => {
385 queue.remove(idx);
386 }
387 Eviction::DropIncoming => {
388 return;
391 }
392 Eviction::DropCorrelatedLoud(idx) => {
393 if let Some(dropped) = queue.remove(idx) {
394 log_correlation_drop(event_id, &dropped);
395 }
396 }
397 }
398 }
399 queue.push_back(event);
400 }
401
402 pub(crate) fn next(&mut self) -> Option<EventItem> {
403 if let Some((_size, queue)) = self.event_queues.get_mut(&*NON_INPUT_EVENT_ID)
405 && let Some(event) = queue.pop_front()
406 {
407 return Some(event);
408 }
409
410 for index in 0..self.last_used.len() {
414 let id = &self.last_used[index];
415 if let Some((_size, queue)) = self.event_queues.get_mut(id)
416 && let Some(event) = queue.pop_front()
417 {
418 if let Some(id) = self.last_used.remove(index) {
420 self.last_used.push_back(id);
421 }
422 return Some(event);
423 }
424 }
425
426 None
427 }
428
429 pub(crate) fn is_empty(&self) -> bool {
430 self.event_queues
431 .iter()
432 .all(|(_id, (_size, queue))| queue.is_empty())
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use crate::uhlc;
440 use dora_message::{
441 daemon_to_node::NodeEvent,
442 metadata::{FLUSH, Metadata, MetadataParameters, Parameter},
443 };
444
445 fn make_input(id: &str, params: MetadataParameters) -> EventItem {
446 let ts = uhlc::HLC::default().new_timestamp();
447 let metadata = Metadata::from_parameters(ts, params);
448 EventItem::NodeEvent {
449 event: NodeEvent::Input {
450 id: DataId::from(id.to_string()),
451 metadata: std::sync::Arc::new(metadata),
452 data: None,
453 },
454 }
455 }
456
457 fn make_stop() -> EventItem {
458 EventItem::NodeEvent {
459 event: NodeEvent::Stop,
460 }
461 }
462
463 fn make_input_closed(id: &str) -> EventItem {
464 EventItem::NodeEvent {
465 event: NodeEvent::InputClosed {
466 id: DataId::from(id.to_string()),
467 },
468 }
469 }
470
471 fn make_scheduler(audio_capacity: usize) -> (Scheduler, DataId) {
472 let id = DataId::from("audio".to_string());
473 let mut queues = HashMap::new();
474 queues.insert(id.clone(), (audio_capacity, VecDeque::new()));
475 queues.insert(
476 DataId::from(NON_INPUT_EVENT.to_string()),
477 (10, VecDeque::new()),
478 );
479 (Scheduler::with_policies(queues, HashMap::new()), id)
480 }
481
482 #[test]
483 fn flush_clears_older_queued_events() {
484 let (mut sched, id) = make_scheduler(10);
485
486 sched.add_event(make_input("audio", MetadataParameters::new()));
487 sched.add_event(make_input("audio", MetadataParameters::new()));
488 sched.add_event(make_input("audio", MetadataParameters::new()));
489 assert_eq!(sched.event_queues[&id].1.len(), 3);
490
491 let mut flush_params = MetadataParameters::new();
493 flush_params.insert(FLUSH.into(), Parameter::Bool(true));
494 sched.add_event(make_input("audio", flush_params));
495
496 assert_eq!(sched.event_queues[&id].1.len(), 1);
497 }
498
499 #[test]
500 fn non_flush_does_not_clear_queue() {
501 let (mut sched, id) = make_scheduler(10);
502
503 sched.add_event(make_input("audio", MetadataParameters::new()));
504 sched.add_event(make_input("audio", MetadataParameters::new()));
505 sched.add_event(make_input("audio", MetadataParameters::new()));
506 assert_eq!(sched.event_queues[&id].1.len(), 3);
507 }
508
509 #[test]
510 fn flush_false_does_not_clear_queue() {
511 let (mut sched, id) = make_scheduler(10);
512
513 sched.add_event(make_input("audio", MetadataParameters::new()));
514 sched.add_event(make_input("audio", MetadataParameters::new()));
515
516 let mut params = MetadataParameters::new();
517 params.insert(FLUSH.into(), Parameter::Bool(false));
518 sched.add_event(make_input("audio", params));
519
520 assert_eq!(sched.event_queues[&id].1.len(), 3);
521 }
522
523 #[test]
524 fn flush_with_queue_size_one_retains_flush_message() {
525 let (mut sched, id) = make_scheduler(1);
526
527 sched.add_event(make_input("audio", MetadataParameters::new()));
528 assert_eq!(sched.event_queues[&id].1.len(), 1);
529
530 let mut flush_params = MetadataParameters::new();
532 flush_params.insert(FLUSH.into(), Parameter::Bool(true));
533 sched.add_event(make_input("audio", flush_params));
534
535 assert_eq!(sched.event_queues[&id].1.len(), 1);
537 }
538
539 #[test]
540 fn drop_oldest_tracks_drop_count() {
541 let (mut sched, id) = make_scheduler(2);
542
543 sched.add_event(make_input("audio", MetadataParameters::new()));
545 sched.add_event(make_input("audio", MetadataParameters::new()));
546 assert_eq!(sched.event_queues[&id].1.len(), 2);
547
548 sched.add_event(make_input("audio", MetadataParameters::new()));
550 sched.add_event(make_input("audio", MetadataParameters::new()));
551 sched.add_event(make_input("audio", MetadataParameters::new()));
552
553 assert_eq!(sched.event_queues[&id].1.len(), 2);
555
556 let counts = sched.drain_drop_counts();
558 assert_eq!(counts.get(&id), Some(&3));
559
560 let counts = sched.drain_drop_counts();
562 assert!(counts.is_empty());
563 }
564
565 #[test]
568 fn flush_retains_correlated_events() {
569 let (mut sched, id) = make_scheduler(10);
572
573 sched.add_event(make_input("audio", with_request_id("req-1")));
574 sched.add_event(make_input("audio", MetadataParameters::new()));
575 sched.add_event(make_input("audio", MetadataParameters::new()));
576 assert_eq!(sched.event_queues[&id].1.len(), 3);
577
578 let mut flush_params = MetadataParameters::new();
579 flush_params.insert(FLUSH.into(), Parameter::Bool(true));
580 sched.add_event(make_input("audio", flush_params));
581
582 let queue = &sched.event_queues[&id].1;
583 assert_eq!(queue.len(), 2);
585 assert!(
586 queue
587 .iter()
588 .any(|e| request_id_of(e).as_deref() == Some("req-1")),
589 "service response with request_id was wiped by flush"
590 );
591 }
592
593 #[test]
594 fn flush_retains_goal_id_events() {
595 let (mut sched, id) = make_scheduler(10);
597
598 let mut goal_params = MetadataParameters::new();
599 goal_params.insert(GOAL_ID.into(), Parameter::String("goal-7".to_string()));
600 sched.add_event(make_input("audio", goal_params));
601 sched.add_event(make_input("audio", MetadataParameters::new()));
602
603 let mut flush_params = MetadataParameters::new();
604 flush_params.insert(FLUSH.into(), Parameter::Bool(true));
605 sched.add_event(make_input("audio", flush_params));
606
607 let queue = &sched.event_queues[&id].1;
608 assert_eq!(queue.len(), 2);
610 let has_goal = queue.iter().any(|e| {
611 let EventItem::NodeEvent {
612 event: NodeEvent::Input { metadata, .. },
613 ..
614 } = e
615 else {
616 return false;
617 };
618 get_string_param(&metadata.parameters, GOAL_ID) == Some("goal-7")
619 });
620 assert!(has_goal, "action result with goal_id was wiped by flush");
621 }
622
623 #[test]
624 fn flush_with_all_correlated_queue_keeps_everything() {
625 let (mut sched, id) = make_scheduler(10);
628
629 sched.add_event(make_input("audio", with_request_id("req-1")));
630 sched.add_event(make_input("audio", with_request_id("req-2")));
631 sched.add_event(make_input("audio", with_request_id("req-3")));
632
633 let mut flush_params = MetadataParameters::new();
634 flush_params.insert(FLUSH.into(), Parameter::Bool(true));
635 sched.add_event(make_input("audio", flush_params));
636
637 assert_eq!(sched.event_queues[&id].1.len(), 4);
639 }
640
641 fn request_id_of(event: &EventItem) -> Option<String> {
645 let EventItem::NodeEvent {
646 event: NodeEvent::Input { metadata, .. },
647 ..
648 } = event
649 else {
650 return None;
651 };
652 get_string_param(&metadata.parameters, REQUEST_ID).map(|s| s.to_string())
653 }
654
655 fn with_request_id(id: &str) -> MetadataParameters {
656 let mut params = MetadataParameters::new();
657 params.insert(REQUEST_ID.into(), Parameter::String(id.to_string()));
658 params
659 }
660
661 #[test]
662 fn drop_oldest_preserves_correlated_when_non_correlated_present() {
663 let (mut sched, id) = make_scheduler(3);
666
667 sched.add_event(make_input("audio", with_request_id("req-1")));
668 sched.add_event(make_input("audio", MetadataParameters::new()));
669 sched.add_event(make_input("audio", MetadataParameters::new()));
670 sched.add_event(make_input("audio", MetadataParameters::new()));
671
672 let queue = &sched.event_queues[&id].1;
673 assert_eq!(queue.len(), 3);
674 assert!(
676 queue
677 .iter()
678 .any(|e| request_id_of(e).as_deref() == Some("req-1")),
679 "correlated message was dropped even though non-correlated events were available"
680 );
681 }
682
683 #[test]
684 fn drop_oldest_drops_middle_non_correlated_to_save_front_correlated() {
685 let (mut sched, id) = make_scheduler(3);
688
689 sched.add_event(make_input("audio", with_request_id("req-1")));
690 sched.add_event(make_input("audio", MetadataParameters::new()));
691 sched.add_event(make_input("audio", with_request_id("req-2")));
692 sched.add_event(make_input("audio", MetadataParameters::new()));
693
694 let queue = &sched.event_queues[&id].1;
695 assert_eq!(queue.len(), 3);
696 assert!(
697 queue
698 .iter()
699 .any(|e| request_id_of(e).as_deref() == Some("req-1"))
700 );
701 assert!(
702 queue
703 .iter()
704 .any(|e| request_id_of(e).as_deref() == Some("req-2"))
705 );
706 }
707
708 #[test]
709 fn drop_oldest_drops_incoming_if_queue_is_fully_correlated_and_incoming_is_not() {
710 let (mut sched, id) = make_scheduler(2);
713
714 sched.add_event(make_input("audio", with_request_id("req-1")));
715 sched.add_event(make_input("audio", with_request_id("req-2")));
716 sched.add_event(make_input("audio", MetadataParameters::new()));
717
718 let queue = &sched.event_queues[&id].1;
719 assert_eq!(queue.len(), 2);
720 let ids: Vec<_> = queue.iter().filter_map(request_id_of).collect();
721 assert_eq!(ids, vec!["req-1".to_string(), "req-2".to_string()]);
722
723 let counts = sched.drain_drop_counts();
725 assert_eq!(counts.get(&id), Some(&1));
726 }
727
728 #[test]
729 fn drop_oldest_drops_front_loudly_when_both_queue_and_incoming_are_correlated() {
730 let (mut sched, id) = make_scheduler(2);
733
734 sched.add_event(make_input("audio", with_request_id("req-1")));
735 sched.add_event(make_input("audio", with_request_id("req-2")));
736 sched.add_event(make_input("audio", with_request_id("req-3")));
737
738 let queue = &sched.event_queues[&id].1;
739 assert_eq!(queue.len(), 2);
740 let ids: Vec<_> = queue.iter().filter_map(request_id_of).collect();
741 assert_eq!(ids, vec!["req-2".to_string(), "req-3".to_string()]);
742 }
743
744 #[test]
745 fn drop_oldest_goal_id_is_also_preserved() {
746 let (mut sched, id) = make_scheduler(2);
748
749 let mut goal_params = MetadataParameters::new();
750 goal_params.insert(GOAL_ID.into(), Parameter::String("goal-42".to_string()));
751
752 sched.add_event(make_input("audio", goal_params));
753 sched.add_event(make_input("audio", MetadataParameters::new()));
754 sched.add_event(make_input("audio", MetadataParameters::new()));
755
756 let queue = &sched.event_queues[&id].1;
757 assert_eq!(queue.len(), 2);
758 let has_goal = queue.iter().any(|e| {
759 let EventItem::NodeEvent {
760 event: NodeEvent::Input { metadata, .. },
761 ..
762 } = e
763 else {
764 return false;
765 };
766 get_string_param(&metadata.parameters, GOAL_ID) == Some("goal-42")
767 });
768 assert!(
769 has_goal,
770 "goal-42 was dropped despite having non-correlated events to drop"
771 );
772 }
773
774 #[test]
781 fn stop_survives_non_input_queue_overflow() {
782 let (mut sched, _id) = make_scheduler(10);
784
785 sched.add_event(make_stop());
786 for i in 0..100 {
787 sched.add_event(make_input_closed(&format!("in-{i}")));
788 }
789
790 let non_input = &sched.event_queues[&*NON_INPUT_EVENT_ID].1;
791 assert_eq!(non_input.len(), 10, "non-input queue must stay bounded");
792 assert!(
793 non_input.iter().any(is_stop),
794 "the Stop event must survive non-input queue overflow"
795 );
796 }
797
798 #[test]
802 fn incoming_stop_is_admitted_into_a_full_non_input_queue() {
803 let (mut sched, _id) = make_scheduler(10);
804
805 for i in 0..10 {
806 sched.add_event(make_input_closed(&format!("in-{i}")));
807 }
808 sched.add_event(make_stop());
809
810 let non_input = &sched.event_queues[&*NON_INPUT_EVENT_ID].1;
811 assert_eq!(non_input.len(), 10, "non-input queue must stay bounded");
812 assert!(
813 non_input.iter().any(is_stop),
814 "an incoming Stop must be admitted into a full non-input queue"
815 );
816 }
817
818 #[test]
823 fn flush_retains_stop_when_targeting_the_non_input_queue() {
824 let (mut sched, _id) = make_scheduler(10);
825
826 sched.add_event(make_stop());
827 sched.add_event(make_input_closed("x"));
828
829 let mut flush_params = MetadataParameters::new();
830 flush_params.insert(FLUSH.into(), Parameter::Bool(true));
831 sched.add_event(make_input(NON_INPUT_EVENT, flush_params));
832
833 let non_input = &sched.event_queues[&*NON_INPUT_EVENT_ID].1;
834 assert!(
835 non_input.iter().any(is_stop),
836 "flush must not evict the Stop shutdown signal"
837 );
838 }
839
840 #[test]
841 fn backpressure_policy_prevents_drops() {
842 let id = DataId::from("commands".to_string());
843 let mut queues = HashMap::new();
844 queues.insert(id.clone(), (2, VecDeque::new()));
845 queues.insert(
846 DataId::from(NON_INPUT_EVENT.to_string()),
847 (10, VecDeque::new()),
848 );
849 let policies = HashMap::from([(id.clone(), QueuePolicy::Backpressure)]);
850 let mut sched = Scheduler::with_policies(queues, policies);
851
852 sched.add_event(make_input("commands", MetadataParameters::new()));
854 sched.add_event(make_input("commands", MetadataParameters::new()));
855 sched.add_event(make_input("commands", MetadataParameters::new()));
856 sched.add_event(make_input("commands", MetadataParameters::new()));
857
858 assert_eq!(sched.event_queues[&id].1.len(), 4);
860
861 let counts = sched.drain_drop_counts();
863 assert!(counts.is_empty());
864 }
865
866 fn make_zenoh_input(id: &str, params: MetadataParameters) -> EventItem {
869 let ts = uhlc::HLC::default().new_timestamp();
870 let metadata = Metadata::from_parameters(ts, params);
871 use dora_arrow_convert::IntoArrow;
872 EventItem::ZenohInput {
873 id: DataId::from(id.to_string()),
874 metadata: std::sync::Arc::new(metadata),
875 data: dora_arrow_convert::internal::into_array_ref(().into_arrow()).to_data(),
876 }
877 }
878
879 fn request_id_of_zenoh(event: &EventItem) -> Option<String> {
881 let EventItem::ZenohInput { metadata, .. } = event else {
882 return None;
883 };
884 get_string_param(&metadata.parameters, REQUEST_ID).map(|s| s.to_string())
885 }
886
887 #[test]
888 fn zenoh_drop_oldest_drops_front_loudly_when_both_queue_and_incoming_are_correlated() {
889 let (mut sched, id) = make_scheduler(2);
896
897 sched.add_event(make_zenoh_input("audio", with_request_id("req-1")));
898 sched.add_event(make_zenoh_input("audio", with_request_id("req-2")));
899 sched.add_event(make_zenoh_input("audio", with_request_id("req-3")));
900
901 let queue = &sched.event_queues[&id].1;
902 assert_eq!(queue.len(), 2);
903 let ids: Vec<_> = queue.iter().filter_map(request_id_of_zenoh).collect();
904 assert_eq!(ids, vec!["req-2".to_string(), "req-3".to_string()]);
905 }
906
907 #[test]
908 fn zenoh_drop_oldest_preserves_correlated_when_non_correlated_present() {
909 let (mut sched, id) = make_scheduler(3);
912
913 sched.add_event(make_zenoh_input("audio", with_request_id("req-1")));
914 sched.add_event(make_zenoh_input("audio", MetadataParameters::new()));
915 sched.add_event(make_zenoh_input("audio", MetadataParameters::new()));
916 sched.add_event(make_zenoh_input("audio", MetadataParameters::new()));
917
918 let queue = &sched.event_queues[&id].1;
919 assert_eq!(queue.len(), 3);
920 assert!(
921 queue
922 .iter()
923 .any(|e| request_id_of_zenoh(e).as_deref() == Some("req-1")),
924 "correlated ZenohInput was dropped even though non-correlated events were available"
925 );
926 }
927}