1use std::{borrow::Cow, time::Instant};
2
3use saluki_common::collections::FastHashMap;
4use saluki_error::{generic_error, GenericError};
5use saluki_metrics::{static_metrics, Counter, Histogram};
6use tokio::sync::mpsc;
7
8use super::Dispatchable;
9use crate::{components::ComponentContext, topology::OutputName};
10
11#[static_metrics(prefix = component, labels(component_id, component_type, output))]
14#[derive(Clone)]
15struct DispatcherMetrics {
16 events_sent_total: Counter,
17 #[metric(level = trace)]
18 send_latency_seconds: Histogram,
19 events_discarded_total: Counter,
20}
21
22impl DispatcherMetrics {
23 fn default_output(context: ComponentContext) -> Self {
24 Self::with_output_name(context, "_default")
25 }
26
27 fn named_output(context: ComponentContext, output_name: &str) -> Self {
28 Self::with_output_name(context, output_name)
29 }
30
31 fn with_output_name(context: ComponentContext, output_name: &str) -> Self {
32 Self::new(context.component_id(), context.component_type().as_str(), output_name)
33 }
34}
35
36pub trait DispatchBuffer: Dispatchable + Default {
38 type Item;
40
41 fn len(&self) -> usize;
43
44 fn is_full(&self) -> bool;
46
47 fn try_push(&mut self, item: Self::Item) -> Option<Self::Item>;
51}
52
53struct DispatchTarget<T> {
54 metrics: DispatcherMetrics,
55 senders: Vec<mpsc::Sender<T>>,
56}
57
58impl<T> DispatchTarget<T>
59where
60 T: Dispatchable,
61{
62 fn default_output(context: ComponentContext) -> Self {
63 Self {
64 metrics: DispatcherMetrics::default_output(context),
65 senders: Vec::new(),
66 }
67 }
68
69 fn named_output(context: ComponentContext, output_name: &str) -> Self {
70 Self {
71 metrics: DispatcherMetrics::named_output(context, output_name),
72 senders: Vec::new(),
73 }
74 }
75
76 fn add_sender(&mut self, sender: mpsc::Sender<T>) {
77 self.senders.push(sender);
78 }
79
80 async fn send(&self, item: T) -> Result<(), GenericError> {
81 if self.senders.is_empty() {
82 let item_count = item.item_count() as u64;
84 self.metrics.events_discarded_total().increment(item_count);
85 saluki_antithesis::sometimes!(
88 true,
89 "events discarded on a zero-sender output",
90 { "items": item_count }
91 );
92 return Ok(());
93 }
94
95 let start = Instant::now();
96 let item_count = item.item_count();
97
98 saluki_antithesis::always_gt!(self.senders.len(), 0, "dispatcher fanout has at least one sender");
100 let cloned_sends = self.senders.len() - 1;
101 for sender in &self.senders[0..cloned_sends] {
102 sender
103 .send(item.clone())
104 .await
105 .map_err(|_| generic_error!("Failed to send to output."))?;
106 }
107
108 let last_sender = &self.senders[cloned_sends];
110 last_sender
111 .send(item)
112 .await
113 .map_err(|_| generic_error!("Failed to send to output."))?;
114
115 let elapsed = start.elapsed();
116
117 self.metrics.send_latency_seconds().record(elapsed);
120
121 let total_events_sent = (self.senders.len() * item_count) as u64;
122 self.metrics.events_sent_total().increment(total_events_sent);
123
124 Ok(())
125 }
126}
127
128pub struct BufferedDispatcher<'a, T> {
135 metrics: &'a DispatcherMetrics,
136 flushed_len: usize,
137 buffer: Option<T>,
138 target: &'a DispatchTarget<T>,
139}
140
141impl<'a, T> BufferedDispatcher<'a, T> {
142 fn new(target: &'a DispatchTarget<T>) -> Self {
143 Self {
144 metrics: &target.metrics,
145 flushed_len: 0,
146 buffer: None,
147 target,
148 }
149 }
150}
151
152impl<T> BufferedDispatcher<'_, T>
153where
154 T: DispatchBuffer,
155{
156 async fn try_flush_buffer(&self, buffer: T) -> Result<(), GenericError> {
157 let buffer_len = buffer.len();
158 if buffer_len > 0 {
159 self.target.send(buffer).await
160 } else {
161 Ok(())
162 }
163 }
164
165 pub async fn push(&mut self, item: T::Item) -> Result<(), GenericError> {
172 if let Some(old_buffer) = self.buffer.take_if(|b| b.is_full()) {
174 self.try_flush_buffer(old_buffer).await?;
175 }
176
177 let buffer = self.buffer.get_or_insert_default();
182 if buffer.try_push(item).is_some() {
183 return Err(generic_error!("Dispatch buffer already full after acquisition."));
184 }
185
186 self.flushed_len += 1;
187
188 Ok(())
189 }
190
191 pub async fn send_all<I>(mut self, items: I) -> Result<usize, GenericError>
200 where
201 I: IntoIterator<Item = T::Item>,
202 {
203 for item in items {
204 self.push(item).await?;
205 }
206
207 self.flush().await
208 }
209
210 pub async fn flush(mut self) -> Result<usize, GenericError> {
219 if let Some(old_buffer) = self.buffer.take() {
220 self.try_flush_buffer(old_buffer).await?;
221 }
222
223 self.metrics.events_sent_total().increment(self.flushed_len as u64);
226
227 Ok(self.flushed_len)
228 }
229}
230
231pub struct Dispatcher<T>
237where
238 T: Dispatchable,
239{
240 context: ComponentContext,
241 default: Option<DispatchTarget<T>>,
242 targets: FastHashMap<Cow<'static, str>, DispatchTarget<T>>,
243}
244
245impl<T> Dispatcher<T>
246where
247 T: Dispatchable,
248{
249 pub fn new(context: ComponentContext) -> Self {
251 Self {
252 context,
253 default: None,
254 targets: FastHashMap::default(),
255 }
256 }
257
258 pub fn add_output(&mut self, output_name: OutputName) -> Result<(), GenericError> {
264 match output_name {
265 OutputName::Default => {
266 if self.default.is_some() {
267 return Err(generic_error!("Default output already exists."));
268 }
269
270 self.default = Some(DispatchTarget::default_output(self.context.clone()));
271 }
272 OutputName::Given(name) => {
273 if self.targets.contains_key(&name) {
274 return Err(generic_error!("Output '{}' already exists.", name));
275 }
276 let target = DispatchTarget::named_output(self.context.clone(), &name);
277 self.targets.insert(name, target);
278 }
279 }
280
281 Ok(())
282 }
283
284 pub fn attach_sender_to_output(
290 &mut self, output_name: &OutputName, sender: mpsc::Sender<T>,
291 ) -> Result<(), GenericError> {
292 let target = match output_name {
293 OutputName::Default => self
294 .default
295 .as_mut()
296 .ok_or_else(|| generic_error!("No default output declared."))?,
297 OutputName::Given(name) => self
298 .targets
299 .get_mut(name)
300 .ok_or_else(|| generic_error!("Output '{}' does not exist.", name))?,
301 };
302 target.add_sender(sender);
303
304 Ok(())
305 }
306
307 fn get_default_output(&self) -> Result<&DispatchTarget<T>, GenericError> {
308 self.default
309 .as_ref()
310 .ok_or_else(|| generic_error!("No default output declared."))
311 }
312
313 fn get_named_output(&self, name: &str) -> Result<&DispatchTarget<T>, GenericError> {
314 self.targets
315 .get(name)
316 .ok_or_else(|| generic_error!("No output named '{}' declared.", name))
317 }
318
319 pub fn is_default_output_connected(&self) -> bool {
321 self.default.as_ref().is_some_and(|target| !target.senders.is_empty())
322 }
323
324 pub fn is_named_output_connected(&self, name: &str) -> bool {
326 self.targets.get(name).is_some_and(|target| !target.senders.is_empty())
327 }
328
329 pub async fn dispatch(&self, item: T) -> Result<(), GenericError> {
335 self.dispatch_inner(None, item).await
336 }
337
338 pub async fn dispatch_named<N>(&self, output_name: N, item: T) -> Result<(), GenericError>
344 where
345 N: AsRef<str>,
346 {
347 self.dispatch_inner(Some(output_name.as_ref()), item).await
348 }
349
350 async fn dispatch_inner(&self, output_name: Option<&str>, item: T) -> Result<(), GenericError> {
351 let target = match output_name {
352 None => self.get_default_output()?,
353 Some(name) => self.get_named_output(name)?,
354 };
355
356 target.send(item).await?;
357
358 Ok(())
359 }
360}
361
362impl<T> Dispatcher<T>
363where
364 T: DispatchBuffer,
365{
366 pub fn buffered(&self) -> Result<BufferedDispatcher<'_, T>, GenericError> {
376 self.get_default_output().map(BufferedDispatcher::new)
377 }
378
379 pub fn buffered_named<N>(&self, output_name: N) -> Result<BufferedDispatcher<'_, T>, GenericError>
389 where
390 N: AsRef<str>,
391 {
392 self.get_named_output(output_name.as_ref()).map(BufferedDispatcher::new)
393 }
394
395 pub async fn dispatch_one(&self, item: T::Item) -> Result<(), GenericError> {
401 self.dispatch_one_inner(None, item).await
402 }
403
404 pub async fn dispatch_one_named<N>(&self, output_name: N, item: T::Item) -> Result<(), GenericError>
410 where
411 N: AsRef<str>,
412 {
413 self.dispatch_one_inner(Some(output_name.as_ref()), item).await
414 }
415
416 async fn dispatch_one_inner(&self, output_name: Option<&str>, item: T::Item) -> Result<(), GenericError> {
417 let target = match output_name {
418 None => self.get_default_output()?,
419 Some(name) => self.get_named_output(name)?,
420 };
421
422 let mut buffer = T::default();
423 if buffer.try_push(item).is_some() {
424 return Err(generic_error!("Default-constructed buffer rejected a single item."));
425 }
426 target.send(buffer).await
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use std::ops::Deref;
435
436 use metrics::{Key, Label};
437 use metrics_util::{
438 debugging::{DebugValue, DebuggingRecorder, Snapshotter},
439 CompositeKey, MetricKind,
440 };
441 use ordered_float::OrderedFloat;
442
443 use super::*;
444
445 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
446 struct SingleEvent<T>(T);
447
448 impl<T: Clone + Copy> Dispatchable for SingleEvent<T> {
449 fn item_count(&self) -> usize {
450 1
451 }
452 }
453
454 impl<T: Clone + Copy> From<T> for SingleEvent<T> {
455 fn from(value: T) -> Self {
456 Self(value)
457 }
458 }
459
460 #[derive(Clone, Debug, Eq, PartialEq)]
461 struct FixedUsizeVec<const N: usize> {
462 data: [usize; N],
463 len: usize,
464 }
465
466 impl<const N: usize> Default for FixedUsizeVec<N> {
467 fn default() -> Self {
468 Self { data: [0; N], len: 0 }
469 }
470 }
471
472 impl<const N: usize> Deref for FixedUsizeVec<N> {
473 type Target = [usize];
474
475 fn deref(&self) -> &Self::Target {
476 &self.data
477 }
478 }
479
480 impl<const N: usize> Dispatchable for FixedUsizeVec<N> {
481 fn item_count(&self) -> usize {
482 self.len
483 }
484 }
485
486 impl<const N: usize> DispatchBuffer for FixedUsizeVec<N> {
487 type Item = usize;
488
489 fn len(&self) -> usize {
490 self.len
491 }
492
493 fn is_full(&self) -> bool {
494 self.len == N
495 }
496
497 fn try_push(&mut self, item: Self::Item) -> Option<Self::Item> {
498 if self.is_full() {
499 Some(item)
500 } else {
501 self.data[self.len] = item;
502 self.len += 1;
503 None
504 }
505 }
506 }
507
508 fn unbuffered_dispatcher<T: Dispatchable>() -> Dispatcher<T> {
509 let component_context = ComponentContext::test_source("dispatcher_test");
510 Dispatcher::new(component_context)
511 }
512
513 fn buffered_dispatcher<T: DispatchBuffer>() -> Dispatcher<T> {
514 unbuffered_dispatcher()
515 }
516
517 struct OutputCase {
522 output: OutputName,
523 metric_label: &'static str,
524 }
525
526 fn output_cases() -> [OutputCase; 2] {
527 [
528 OutputCase {
529 output: OutputName::Default,
530 metric_label: "_default",
531 },
532 OutputCase {
533 output: OutputName::Given("special".into()),
534 metric_label: "special",
535 },
536 ]
537 }
538
539 fn add_output_with_senders<T: Dispatchable, const N: usize>(
541 dispatcher: &mut Dispatcher<T>, output: &OutputName, senders: [mpsc::Sender<T>; N],
542 ) {
543 dispatcher
544 .add_output(output.clone())
545 .expect("output should not be declared yet");
546 for sender in senders {
547 dispatcher
548 .attach_sender_to_output(output, sender)
549 .expect("output should exist after being declared");
550 }
551 }
552
553 async fn dispatch_to<T: Dispatchable>(
555 dispatcher: &Dispatcher<T>, output: &OutputName, item: T,
556 ) -> Result<(), GenericError> {
557 match output {
558 OutputName::Default => dispatcher.dispatch(item).await,
559 OutputName::Given(name) => dispatcher.dispatch_named(name.as_ref(), item).await,
560 }
561 }
562
563 fn buffered_for<'a, T: DispatchBuffer>(
565 dispatcher: &'a Dispatcher<T>, output: &OutputName,
566 ) -> Result<BufferedDispatcher<'a, T>, GenericError> {
567 match output {
568 OutputName::Default => dispatcher.buffered(),
569 OutputName::Given(name) => dispatcher.buffered_named(name.as_ref()),
570 }
571 }
572
573 async fn dispatch_one_to<T: DispatchBuffer>(
575 dispatcher: &Dispatcher<T>, output: &OutputName, item: T::Item,
576 ) -> Result<(), GenericError> {
577 match output {
578 OutputName::Default => dispatcher.dispatch_one(item).await,
579 OutputName::Given(name) => dispatcher.dispatch_one_named(name.as_ref(), item).await,
580 }
581 }
582
583 fn is_output_connected<T: Dispatchable>(dispatcher: &Dispatcher<T>, output: &OutputName) -> bool {
585 match output {
586 OutputName::Default => dispatcher.is_default_output_connected(),
587 OutputName::Given(name) => dispatcher.is_named_output_connected(name.as_ref()),
588 }
589 }
590
591 fn get_dispatcher_metric_ckey(
592 kind: MetricKind, name: &'static str, output_name: &'static str, tags: &[(&'static str, &'static str)],
593 ) -> CompositeKey {
594 let mut labels = vec![
595 Label::from_static_parts("component_id", "dispatcher_test"),
596 Label::from_static_parts("component_type", "source"),
597 Label::from_static_parts("output", output_name),
598 ];
599
600 for tag in tags {
601 labels.push(Label::from_static_parts(tag.0, tag.1));
602 }
603
604 let key = Key::from_parts(name, labels);
605 CompositeKey::new(kind, key)
606 }
607
608 fn get_output_metrics(snapshotter: &Snapshotter, output_name: &'static str) -> (u64, u64, Vec<OrderedFloat<f64>>) {
609 let events_sent_key = get_dispatcher_metric_ckey(
610 MetricKind::Counter,
611 DispatcherMetrics::events_sent_total_name(),
612 output_name,
613 &[],
614 );
615 let events_discarded_key = get_dispatcher_metric_ckey(
616 MetricKind::Counter,
617 DispatcherMetrics::events_discarded_total_name(),
618 output_name,
619 &[],
620 );
621 let send_latency_key = get_dispatcher_metric_ckey(
622 MetricKind::Histogram,
623 DispatcherMetrics::send_latency_seconds_name(),
624 output_name,
625 &[],
626 );
627
628 let current_metrics = snapshotter.snapshot().into_hashmap();
630 let (_, _, events_sent) = current_metrics
631 .get(&events_sent_key)
632 .expect("should have events sent metric");
633 let (_, _, events_discarded) = current_metrics
634 .get(&events_discarded_key)
635 .expect("should have events discarded metric");
636 let (_, _, send_latency) = current_metrics
637 .get(&send_latency_key)
638 .expect("should have send latency metric");
639
640 let events_sent = match events_sent {
641 DebugValue::Counter(value) => *value,
642 _ => panic!("unexpected metric type for events sent"),
643 };
644
645 let events_discarded = match events_discarded {
646 DebugValue::Counter(value) => *value,
647 _ => panic!("unexpected metric type for events discarded"),
648 };
649
650 let send_latency = match send_latency {
651 DebugValue::Histogram(value) => value.clone(),
652 _ => panic!("unexpected metric type for send latency"),
653 };
654
655 (events_sent, events_discarded, send_latency)
656 }
657
658 #[tokio::test]
659 async fn dispatch_delivers_item_to_each_output_kind() {
660 for case in output_cases() {
662 let mut dispatcher = unbuffered_dispatcher::<SingleEvent<usize>>();
663 let (tx, mut rx) = mpsc::channel(1);
664 add_output_with_senders(&mut dispatcher, &case.output, [tx]);
665
666 let input_item = 42.into();
667 dispatch_to(&dispatcher, &case.output, input_item).await.unwrap();
668
669 let output_item = rx.try_recv().expect("input item should have been dispatched");
670 assert_eq!(output_item, input_item);
671 }
672 }
673
674 #[tokio::test]
675 async fn dispatch_fans_out_to_all_senders_for_each_output_kind() {
676 for case in output_cases() {
678 let mut dispatcher = unbuffered_dispatcher::<SingleEvent<usize>>();
679 let (tx1, mut rx1) = mpsc::channel(1);
680 let (tx2, mut rx2) = mpsc::channel(1);
681 add_output_with_senders(&mut dispatcher, &case.output, [tx1, tx2]);
682
683 let input_item = 42.into();
684 dispatch_to(&dispatcher, &case.output, input_item).await.unwrap();
685
686 assert_eq!(rx1.try_recv().expect("first sender should receive"), input_item);
687 assert_eq!(rx2.try_recv().expect("second sender should receive"), input_item);
688 }
689 }
690
691 #[tokio::test]
692 async fn dispatch_to_unconfigured_output_errors() {
693 for case in output_cases() {
695 let dispatcher = unbuffered_dispatcher::<SingleEvent<()>>();
696 let err = dispatch_to(&dispatcher, &case.output, ().into())
697 .await
698 .expect_err("dispatch to an unconfigured output must fail");
699 let msg = err.to_string();
700 match &case.output {
701 OutputName::Default => assert_eq!(msg, "No default output declared."),
702 OutputName::Given(_) => assert!(msg.contains("No output named"), "got: {msg}"),
703 }
704 }
705 }
706
707 #[tokio::test]
708 async fn buffered_dispatch_flushes_partial_buffer() {
709 for case in output_cases() {
711 let mut dispatcher = buffered_dispatcher::<FixedUsizeVec<4>>();
712 let (tx, mut rx) = mpsc::channel(1);
713 add_output_with_senders(&mut dispatcher, &case.output, [tx]);
714
715 let input_item = 42;
716 let mut buffered = buffered_for(&dispatcher, &case.output).unwrap();
717 buffered.push(input_item).await.unwrap();
718 let flushed_len = buffered.flush().await.unwrap();
719 assert_eq!(flushed_len, 1);
720
721 let output_item = rx.try_recv().expect("input item should have been dispatched");
722 assert_eq!(output_item.len(), 1);
723 assert_eq!(output_item[0], input_item);
724 }
725 }
726
727 #[tokio::test]
728 async fn buffered_dispatch_flushes_full_buffer_during_push() {
729 for case in output_cases() {
732 let mut dispatcher = buffered_dispatcher::<FixedUsizeVec<4>>();
733 let (tx, mut rx) = mpsc::channel(2);
734 add_output_with_senders(&mut dispatcher, &case.output, [tx]);
735
736 let input_items: Vec<usize> = vec![1, 2, 3, 4, 5, 6];
737 let mut buffered = buffered_for(&dispatcher, &case.output).unwrap();
738 for item in &input_items {
739 buffered.push(*item).await.unwrap();
740 }
741 let flushed_len = buffered.flush().await.unwrap();
742 assert_eq!(flushed_len, input_items.len());
743
744 let first = rx.try_recv().expect("first buffer should have been dispatched");
745 assert_eq!(first.len(), 4);
746 assert_eq!(first[0..4], input_items[0..4]);
747
748 let second = rx.try_recv().expect("second buffer should have been dispatched");
749 assert_eq!(second.len(), 2);
750 assert_eq!(second[0..2], input_items[4..6]);
751 }
752 }
753
754 #[tokio::test]
755 async fn buffered_dispatch_fans_out_partial_buffer() {
756 for case in output_cases() {
758 let mut dispatcher = buffered_dispatcher::<FixedUsizeVec<4>>();
759 let (tx1, mut rx1) = mpsc::channel(1);
760 let (tx2, mut rx2) = mpsc::channel(1);
761 add_output_with_senders(&mut dispatcher, &case.output, [tx1, tx2]);
762
763 let input_item = 42;
764 let mut buffered = buffered_for(&dispatcher, &case.output).unwrap();
765 buffered.push(input_item).await.unwrap();
766 let flushed_len = buffered.flush().await.unwrap();
767 assert_eq!(flushed_len, 1);
768
769 let out1 = rx1.try_recv().expect("first sender should receive");
770 assert_eq!(out1.len(), 1);
771 assert_eq!(out1[0], input_item);
772
773 let out2 = rx2.try_recv().expect("second sender should receive");
774 assert_eq!(out2.len(), 1);
775 assert_eq!(out2[0], input_item);
776 }
777 }
778
779 #[tokio::test]
780 async fn dispatch_to_output_without_senders_succeeds() {
781 for case in output_cases() {
783 let mut dispatcher = unbuffered_dispatcher::<SingleEvent<u32>>();
784 dispatcher
785 .add_output(case.output.clone())
786 .expect("should be able to declare the output");
787
788 dispatch_to(&dispatcher, &case.output, 42.into())
789 .await
790 .expect("dispatch to a senderless output should succeed");
791 }
792 }
793
794 #[tokio::test]
795 async fn disconnected_output_records_discarded_events() {
796 for case in output_cases() {
799 let recorder = DebuggingRecorder::new();
800 let snapshotter = recorder.snapshotter();
801 let output = case.output.clone();
802 let dispatcher = metrics::with_local_recorder(&recorder, || {
803 let mut dispatcher = buffered_dispatcher::<FixedUsizeVec<4>>();
804 dispatcher
805 .add_output(output)
806 .expect("should not fail to declare the output");
807 dispatcher
808 });
809
810 let mut single_item = FixedUsizeVec::<4>::default();
812 assert_eq!(None, single_item.try_push(42));
813 let single_item_count = single_item.item_count() as u64;
814 dispatch_to(&dispatcher, &case.output, single_item)
815 .await
816 .expect("should not fail to dispatch");
817
818 let (events_sent, events_discarded, send_latencies) = get_output_metrics(&snapshotter, case.metric_label);
819 assert_eq!(events_sent, 0);
820 assert_eq!(events_discarded, single_item_count);
821 assert!(send_latencies.is_empty());
822
823 let mut multiple_items = FixedUsizeVec::<4>::default();
825 assert_eq!(None, multiple_items.try_push(42));
826 assert_eq!(None, multiple_items.try_push(12345));
827 assert_eq!(None, multiple_items.try_push(1337));
828 let multiple_items_count = multiple_items.item_count() as u64;
829 dispatch_to(&dispatcher, &case.output, multiple_items)
830 .await
831 .expect("should not fail to dispatch");
832
833 let (events_sent, events_discarded, send_latencies) = get_output_metrics(&snapshotter, case.metric_label);
834 assert_eq!(events_sent, 0);
835 assert_eq!(events_discarded, multiple_items_count);
836 assert!(send_latencies.is_empty());
837 }
838 }
839
840 #[tokio::test]
841 async fn output_connected_reflects_sender_attachment() {
842 for case in output_cases() {
845 let mut dispatcher = unbuffered_dispatcher::<SingleEvent<u32>>();
846
847 assert!(
848 !is_output_connected(&dispatcher, &case.output),
849 "an undeclared output must report as not connected"
850 );
851
852 dispatcher
853 .add_output(case.output.clone())
854 .expect("should be able to declare the output");
855 assert!(
856 !is_output_connected(&dispatcher, &case.output),
857 "a declared output with no senders must report as not connected"
858 );
859
860 let (tx, _rx) = mpsc::channel(1);
861 dispatcher
862 .attach_sender_to_output(&case.output, tx)
863 .expect("should be able to attach a sender");
864 assert!(
865 is_output_connected(&dispatcher, &case.output),
866 "an output with a sender attached must report as connected"
867 );
868
869 assert!(
871 !dispatcher.is_named_output_connected("nonexistent_output"),
872 "an unknown named output must report as not connected"
873 );
874 }
875 }
876
877 #[tokio::test]
878 async fn dispatch_one_wraps_item_in_single_element_buffer() {
879 for case in output_cases() {
881 let mut dispatcher = buffered_dispatcher::<FixedUsizeVec<4>>();
882 let (tx, mut rx) = mpsc::channel(1);
883 add_output_with_senders(&mut dispatcher, &case.output, [tx]);
884
885 let input_item = 42;
886 dispatch_one_to(&dispatcher, &case.output, input_item).await.unwrap();
887
888 let output_item = rx.try_recv().expect("input item should have been dispatched");
889 assert_eq!(output_item.len(), 1);
890 assert_eq!(output_item[0], input_item);
891 }
892 }
893
894 #[tokio::test]
895 async fn dispatch_one_to_unconfigured_output_errors() {
896 for case in output_cases() {
899 let dispatcher = buffered_dispatcher::<FixedUsizeVec<4>>();
900 let err = dispatch_one_to(&dispatcher, &case.output, 42)
901 .await
902 .expect_err("dispatch_one to an unconfigured output must fail");
903 let msg = err.to_string();
904 match &case.output {
905 OutputName::Default => assert_eq!(msg, "No default output declared."),
906 OutputName::Given(_) => assert!(msg.contains("No output named"), "got: {msg}"),
907 }
908 }
909 }
910
911 #[tokio::test]
912 async fn add_output_rejects_duplicate() {
913 for case in output_cases() {
916 let mut dispatcher = unbuffered_dispatcher::<SingleEvent<u32>>();
917 dispatcher
918 .add_output(case.output.clone())
919 .expect("first declaration should succeed");
920
921 let err = dispatcher
922 .add_output(case.output.clone())
923 .expect_err("declaring the same output twice must fail");
924 assert!(err.to_string().contains("already exists"), "got: {err}");
925 }
926 }
927
928 #[tokio::test]
929 async fn attach_sender_to_undeclared_output_errors() {
930 for case in output_cases() {
933 let mut dispatcher = unbuffered_dispatcher::<SingleEvent<u32>>();
934 let (tx, _rx) = mpsc::channel(1);
935 let err = dispatcher
936 .attach_sender_to_output(&case.output, tx)
937 .expect_err("attaching to an undeclared output must fail");
938 let msg = err.to_string();
939 match &case.output {
940 OutputName::Default => assert_eq!(msg, "No default output declared."),
941 OutputName::Given(_) => assert!(msg.contains("does not exist"), "got: {msg}"),
942 }
943 }
944 }
945
946 #[tokio::test]
947 async fn dispatch_one_errors_when_buffer_cannot_hold_a_single_item() {
948 let mut dispatcher = buffered_dispatcher::<FixedUsizeVec<0>>();
952 let (tx, _rx) = mpsc::channel(1);
953 add_output_with_senders(&mut dispatcher, &OutputName::Default, [tx]);
954
955 let err = dispatcher
956 .dispatch_one(42)
957 .await
958 .expect_err("a zero-capacity buffer must be rejected, not silently drop the item");
959 assert!(err.to_string().contains("rejected a single item"), "got: {err}");
960 }
961}