Skip to main content

saluki_core/topology/interconnect/
dispatcher.rs

1use std::{borrow::Cow, time::Instant};
2
3use saluki_common::collections::FastHashMap;
4use saluki_error::{generic_error, GenericError};
5use saluki_metrics::static_metrics;
6use tokio::sync::mpsc;
7
8use super::Dispatchable;
9use crate::{components::ComponentContext, topology::OutputName};
10
11// TODO: When we have support for additional static labels on a per-metric basis, add `discard_reason` to
12// `events_discarded_total` metric to indicate that it's due to the destination component being disconnected.
13static_metrics!(
14    name => DispatcherMetrics,
15    prefix => component,
16    labels => [component_id: String, component_type: &'static str, output: String],
17    metrics => [
18        counter(events_sent_total),
19        trace_histogram(send_latency_seconds),
20        counter(events_discarded_total),
21    ],
22);
23
24impl DispatcherMetrics {
25    fn default_output(context: ComponentContext) -> Self {
26        Self::with_output_name(context, "_default")
27    }
28
29    fn named_output(context: ComponentContext, output_name: &str) -> Self {
30        Self::with_output_name(context, output_name)
31    }
32
33    fn with_output_name(context: ComponentContext, output_name: &str) -> Self {
34        Self::new(
35            context.component_id().to_string(),
36            context.component_type().as_str(),
37            output_name.to_string(),
38        )
39    }
40}
41
42/// A type that can be used as a buffer for dispatching items.
43pub trait DispatchBuffer: Dispatchable + Default {
44    /// Type of item that can be pushed into the buffer.
45    type Item;
46
47    /// Returns the number of items currently in the buffer.
48    fn len(&self) -> usize;
49
50    /// Returns `true` if the buffer is full.
51    fn is_full(&self) -> bool;
52
53    /// Attempts to push an item into the buffer.
54    ///
55    /// Returns `Some(item)` if the buffer is full and the item couldn't be pushed.
56    fn try_push(&mut self, item: Self::Item) -> Option<Self::Item>;
57}
58
59struct DispatchTarget<T> {
60    metrics: DispatcherMetrics,
61    senders: Vec<mpsc::Sender<T>>,
62}
63
64impl<T> DispatchTarget<T>
65where
66    T: Dispatchable,
67{
68    fn default_output(context: ComponentContext) -> Self {
69        Self {
70            metrics: DispatcherMetrics::default_output(context),
71            senders: Vec::new(),
72        }
73    }
74
75    fn named_output(context: ComponentContext, output_name: &str) -> Self {
76        Self {
77            metrics: DispatcherMetrics::named_output(context, output_name),
78            senders: Vec::new(),
79        }
80    }
81
82    fn add_sender(&mut self, sender: mpsc::Sender<T>) {
83        self.senders.push(sender);
84    }
85
86    async fn send(&self, item: T) -> Result<(), GenericError> {
87        if self.senders.is_empty() {
88            // Track discarded events when no senders are attached to this output
89            let item_count = item.item_count() as u64;
90            self.metrics.events_discarded_total().increment(item_count);
91            // Anchor the legitimate zero-sender discard. A wired edge never reaches this branch, so this stays a
92            // disconnected-output signal — not a silent-loss-on-a-wired-edge violation.
93            saluki_antithesis::sometimes!(
94                true,
95                "events discarded on a zero-sender output",
96                { "items": item_count }
97            );
98            return Ok(());
99        }
100
101        let start = Instant::now();
102        let item_count = item.item_count();
103
104        // Send the item to all senders except the last one by cloning the item.
105        saluki_antithesis::always_gt!(self.senders.len(), 0, "dispatcher fanout has at least one sender");
106        let cloned_sends = self.senders.len() - 1;
107        for sender in &self.senders[0..cloned_sends] {
108            sender
109                .send(item.clone())
110                .await
111                .map_err(|_| generic_error!("Failed to send to output."))?;
112        }
113
114        // Send the item to the last sender without cloning.
115        let last_sender = &self.senders[cloned_sends];
116        last_sender
117            .send(item)
118            .await
119            .map_err(|_| generic_error!("Failed to send to output."))?;
120
121        let elapsed = start.elapsed();
122
123        // TODO: We should consider splitting this out per-sender somehow. We would need to carry around the
124        // destination component's ID, though, to properly associate it.
125        self.metrics.send_latency_seconds().record(elapsed);
126
127        let total_events_sent = (self.senders.len() * item_count) as u64;
128        self.metrics.events_sent_total().increment(total_events_sent);
129
130        Ok(())
131    }
132}
133
134/// A buffered dispatcher.
135///
136/// `BufferedDispatcher` provides an efficient and ergonomic interface to `Dispatcher` that allows for writing events
137/// one-by-one into batches, which are then dispatched to the configured output as needed. This allows callers to focus
138/// on the logic around what items to send, without needing to worry about the details of event buffer sizing or
139/// flushing.
140pub struct BufferedDispatcher<'a, T> {
141    metrics: &'a DispatcherMetrics,
142    flushed_len: usize,
143    buffer: Option<T>,
144    target: &'a DispatchTarget<T>,
145}
146
147impl<'a, T> BufferedDispatcher<'a, T> {
148    fn new(target: &'a DispatchTarget<T>) -> Self {
149        Self {
150            metrics: &target.metrics,
151            flushed_len: 0,
152            buffer: None,
153            target,
154        }
155    }
156}
157
158impl<T> BufferedDispatcher<'_, T>
159where
160    T: DispatchBuffer,
161{
162    async fn try_flush_buffer(&self, buffer: T) -> Result<(), GenericError> {
163        let buffer_len = buffer.len();
164        if buffer_len > 0 {
165            self.target.send(buffer).await
166        } else {
167            Ok(())
168        }
169    }
170
171    /// Pushes an item into the buffered dispatcher.
172    ///
173    /// # Errors
174    ///
175    /// If there is an error flushing items to the output, or if there is an error acquiring a new buffer, an error
176    /// is returned.
177    pub async fn push(&mut self, item: T::Item) -> Result<(), GenericError> {
178        // If our current buffer is full, flush it before acquiring a new one.
179        if let Some(old_buffer) = self.buffer.take_if(|b| b.is_full()) {
180            self.try_flush_buffer(old_buffer).await?;
181        }
182
183        // Add the item to our current buffer.
184        //
185        // If our current buffer is empty, create a new one first. If the current buffer is full, return an error
186        // because it should be impossible to get a new buffer that is full.
187        let buffer = self.buffer.get_or_insert_default();
188        if buffer.try_push(item).is_some() {
189            return Err(generic_error!("Dispatch buffer already full after acquisition."));
190        }
191
192        self.flushed_len += 1;
193
194        Ok(())
195    }
196
197    /// Consumes this buffered dispatcher and sends/flushes all input items to the underlying output.
198    ///
199    /// If flushing is successful, `Ok(flushed)` is returned, where `flushed` is the total number of items that
200    /// have been flushed through this buffered dispatcher.
201    ///
202    /// # Errors
203    ///
204    /// If there is an error sending items to the output, an error is returned.
205    pub async fn send_all<I>(mut self, items: I) -> Result<usize, GenericError>
206    where
207        I: IntoIterator<Item = T::Item>,
208    {
209        for item in items {
210            self.push(item).await?;
211        }
212
213        self.flush().await
214    }
215
216    /// Consumes this buffered dispatcher, flushing any buffered items to the underlying output.
217    ///
218    /// If flushing is successful, `Ok(flushed)` is returned, where `flushed` is the total number of items that have
219    /// been flushed through this buffered dispatcher.
220    ///
221    /// # Errors
222    ///
223    /// If there is an error sending items to the output, an error is returned.
224    pub async fn flush(mut self) -> Result<usize, GenericError> {
225        if let Some(old_buffer) = self.buffer.take() {
226            self.try_flush_buffer(old_buffer).await?;
227        }
228
229        // We increment the "events sent" metric here because we want to count the number of buffered items, vs doing it in
230        // `DispatchTarget::send` where all it knows is that it sent one item.
231        self.metrics.events_sent_total().increment(self.flushed_len as u64);
232
233        Ok(self.flushed_len)
234    }
235}
236
237/// Dispatches items from one component to another.
238///
239/// [`Dispatcher`] provides an ergonomic interface for sending items to a downstream component. It has support for
240/// multiple outputs (a default output, and additional "named" outputs) and provides telemetry around the number of
241/// dispatched items as well as the latency of sending them.
242pub struct Dispatcher<T>
243where
244    T: Dispatchable,
245{
246    context: ComponentContext,
247    default: Option<DispatchTarget<T>>,
248    targets: FastHashMap<Cow<'static, str>, DispatchTarget<T>>,
249}
250
251impl<T> Dispatcher<T>
252where
253    T: Dispatchable,
254{
255    /// Create a new `Dispatcher` for the given component context.
256    pub fn new(context: ComponentContext) -> Self {
257        Self {
258            context,
259            default: None,
260            targets: FastHashMap::default(),
261        }
262    }
263
264    /// Adds an output to the dispatcher.
265    ///
266    /// # Errors
267    ///
268    /// If the output already exists, an error is returned.
269    pub fn add_output(&mut self, output_name: OutputName) -> Result<(), GenericError> {
270        match output_name {
271            OutputName::Default => {
272                if self.default.is_some() {
273                    return Err(generic_error!("Default output already exists."));
274                }
275
276                self.default = Some(DispatchTarget::default_output(self.context.clone()));
277            }
278            OutputName::Given(name) => {
279                if self.targets.contains_key(&name) {
280                    return Err(generic_error!("Output '{}' already exists.", name));
281                }
282                let target = DispatchTarget::named_output(self.context.clone(), &name);
283                self.targets.insert(name, target);
284            }
285        }
286
287        Ok(())
288    }
289
290    /// Attaches a sender to the given output.
291    ///
292    /// # Errors
293    ///
294    /// If the output doesn't exist, an error is returned.
295    pub fn attach_sender_to_output(
296        &mut self, output_name: &OutputName, sender: mpsc::Sender<T>,
297    ) -> Result<(), GenericError> {
298        let target = match output_name {
299            OutputName::Default => self
300                .default
301                .as_mut()
302                .ok_or_else(|| generic_error!("No default output declared."))?,
303            OutputName::Given(name) => self
304                .targets
305                .get_mut(name)
306                .ok_or_else(|| generic_error!("Output '{}' does not exist.", name))?,
307        };
308        target.add_sender(sender);
309
310        Ok(())
311    }
312
313    fn get_default_output(&self) -> Result<&DispatchTarget<T>, GenericError> {
314        self.default
315            .as_ref()
316            .ok_or_else(|| generic_error!("No default output declared."))
317    }
318
319    fn get_named_output(&self, name: &str) -> Result<&DispatchTarget<T>, GenericError> {
320        self.targets
321            .get(name)
322            .ok_or_else(|| generic_error!("No output named '{}' declared.", name))
323    }
324
325    /// Returns `true` if the default output is connected to downstream components.
326    pub fn is_default_output_connected(&self) -> bool {
327        self.default.as_ref().is_some_and(|target| !target.senders.is_empty())
328    }
329
330    /// Returns `true` if the named output is connected to downstream components.
331    pub fn is_named_output_connected(&self, name: &str) -> bool {
332        self.targets.get(name).is_some_and(|target| !target.senders.is_empty())
333    }
334
335    /// Dispatches the given item to the default output.
336    ///
337    /// # Errors
338    ///
339    /// If the default output isn't set, or there is an error sending to the default output, an error is returned.
340    pub async fn dispatch(&self, item: T) -> Result<(), GenericError> {
341        self.dispatch_inner(None, item).await
342    }
343
344    /// Dispatches the given items to the given named output.
345    ///
346    /// # Errors
347    ///
348    /// If a output of the given name isn't set, or there is an error sending to the output, an error is returned.
349    pub async fn dispatch_named<N>(&self, output_name: N, item: T) -> Result<(), GenericError>
350    where
351        N: AsRef<str>,
352    {
353        self.dispatch_inner(Some(output_name.as_ref()), item).await
354    }
355
356    async fn dispatch_inner(&self, output_name: Option<&str>, item: T) -> Result<(), GenericError> {
357        let target = match output_name {
358            None => self.get_default_output()?,
359            Some(name) => self.get_named_output(name)?,
360        };
361
362        target.send(item).await?;
363
364        Ok(())
365    }
366}
367
368impl<T> Dispatcher<T>
369where
370    T: DispatchBuffer,
371{
372    /// Creates a buffered dispatcher for the default output.
373    ///
374    /// This should generally be used if the items being dispatched aren't already collected in a container, or exposed
375    /// via an iterable type. It allows for efficiently buffering items one-by-one before dispatching them to the
376    /// underlying output.
377    ///
378    /// # Errors
379    ///
380    /// If the default output hasn't been configured, an error will be returned.
381    pub fn buffered(&self) -> Result<BufferedDispatcher<'_, T>, GenericError> {
382        self.get_default_output().map(BufferedDispatcher::new)
383    }
384
385    /// Creates a buffered dispatcher for the given named output.
386    ///
387    /// This should generally be used if the items being dispatched aren't already collected in a container, or exposed
388    /// via an iterable type. It allows for efficiently buffering items one-by-one before dispatching them to the
389    /// underlying output.
390    ///
391    /// # Errors
392    ///
393    /// If the given named output hasn't been configured, an error will be returned.
394    pub fn buffered_named<N>(&self, output_name: N) -> Result<BufferedDispatcher<'_, T>, GenericError>
395    where
396        N: AsRef<str>,
397    {
398        self.get_named_output(output_name.as_ref()).map(BufferedDispatcher::new)
399    }
400
401    /// Dispatches a single item to the default output.
402    ///
403    /// # Errors
404    ///
405    /// If the default output isn't set, or there is an error sending to the default output, an error is returned.
406    pub async fn dispatch_one(&self, item: T::Item) -> Result<(), GenericError> {
407        self.dispatch_one_inner(None, item).await
408    }
409
410    /// Dispatches a single item to the given named output.
411    ///
412    /// # Errors
413    ///
414    /// If an output of the given name isn't set, or there is an error sending to the output, an error is returned.
415    pub async fn dispatch_one_named<N>(&self, output_name: N, item: T::Item) -> Result<(), GenericError>
416    where
417        N: AsRef<str>,
418    {
419        self.dispatch_one_inner(Some(output_name.as_ref()), item).await
420    }
421
422    async fn dispatch_one_inner(&self, output_name: Option<&str>, item: T::Item) -> Result<(), GenericError> {
423        let target = match output_name {
424            None => self.get_default_output()?,
425            Some(name) => self.get_named_output(name)?,
426        };
427
428        let mut buffer = T::default();
429        if buffer.try_push(item).is_some() {
430            return Err(generic_error!("Default-constructed buffer rejected a single item."));
431        }
432        target.send(buffer).await
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    // TODO: Tests asserting we emit metrics, and the right metrics.
439
440    use std::ops::Deref;
441
442    use metrics::{Key, Label};
443    use metrics_util::{
444        debugging::{DebugValue, DebuggingRecorder, Snapshotter},
445        CompositeKey, MetricKind,
446    };
447    use ordered_float::OrderedFloat;
448
449    use super::*;
450
451    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
452    struct SingleEvent<T>(T);
453
454    impl<T: Clone + Copy> Dispatchable for SingleEvent<T> {
455        fn item_count(&self) -> usize {
456            1
457        }
458    }
459
460    impl<T: Clone + Copy> From<T> for SingleEvent<T> {
461        fn from(value: T) -> Self {
462            Self(value)
463        }
464    }
465
466    #[derive(Clone, Debug, Eq, PartialEq)]
467    struct FixedUsizeVec<const N: usize> {
468        data: [usize; N],
469        len: usize,
470    }
471
472    impl<const N: usize> Default for FixedUsizeVec<N> {
473        fn default() -> Self {
474            Self { data: [0; N], len: 0 }
475        }
476    }
477
478    impl<const N: usize> Deref for FixedUsizeVec<N> {
479        type Target = [usize];
480
481        fn deref(&self) -> &Self::Target {
482            &self.data
483        }
484    }
485
486    impl<const N: usize> Dispatchable for FixedUsizeVec<N> {
487        fn item_count(&self) -> usize {
488            self.len
489        }
490    }
491
492    impl<const N: usize> DispatchBuffer for FixedUsizeVec<N> {
493        type Item = usize;
494
495        fn len(&self) -> usize {
496            self.len
497        }
498
499        fn is_full(&self) -> bool {
500            self.len == N
501        }
502
503        fn try_push(&mut self, item: Self::Item) -> Option<Self::Item> {
504            if self.is_full() {
505                Some(item)
506            } else {
507                self.data[self.len] = item;
508                self.len += 1;
509                None
510            }
511        }
512    }
513
514    fn unbuffered_dispatcher<T: Dispatchable>() -> Dispatcher<T> {
515        let component_context = ComponentContext::test_source("dispatcher_test");
516        Dispatcher::new(component_context)
517    }
518
519    fn buffered_dispatcher<T: DispatchBuffer>() -> Dispatcher<T> {
520        unbuffered_dispatcher()
521    }
522
523    fn add_dispatcher_default_output<T: Dispatchable, const N: usize>(
524        dispatcher: &mut Dispatcher<T>, senders: [mpsc::Sender<T>; N],
525    ) {
526        dispatcher
527            .add_output(OutputName::Default)
528            .expect("default output should not be added yet");
529        for sender in senders {
530            dispatcher
531                .attach_sender_to_output(&OutputName::Default, sender)
532                .expect("default output should be added");
533        }
534    }
535
536    fn add_dispatcher_named_output<T: Dispatchable, const N: usize>(
537        dispatcher: &mut Dispatcher<T>, output_name: &'static str, senders: [mpsc::Sender<T>; N],
538    ) {
539        dispatcher
540            .add_output(OutputName::Given(output_name.into()))
541            .expect("named output should not be added yet");
542        for sender in senders {
543            dispatcher
544                .attach_sender_to_output(&OutputName::Given(output_name.into()), sender)
545                .expect("named output should be added");
546        }
547    }
548
549    fn get_dispatcher_metric_ckey(
550        kind: MetricKind, name: &'static str, output_name: &'static str, tags: &[(&'static str, &'static str)],
551    ) -> CompositeKey {
552        let mut labels = vec![
553            Label::from_static_parts("component_id", "dispatcher_test"),
554            Label::from_static_parts("component_type", "source"),
555            Label::from_static_parts("output", output_name),
556        ];
557
558        for tag in tags {
559            labels.push(Label::from_static_parts(tag.0, tag.1));
560        }
561
562        let key = Key::from_parts(name, labels);
563        CompositeKey::new(kind, key)
564    }
565
566    fn get_output_metrics(snapshotter: &Snapshotter, output_name: &'static str) -> (u64, u64, Vec<OrderedFloat<f64>>) {
567        let events_sent_key = get_dispatcher_metric_ckey(
568            MetricKind::Counter,
569            DispatcherMetrics::events_sent_total_name(),
570            output_name,
571            &[],
572        );
573        let events_discarded_key = get_dispatcher_metric_ckey(
574            MetricKind::Counter,
575            DispatcherMetrics::events_discarded_total_name(),
576            output_name,
577            &[],
578        );
579        let send_latency_key = get_dispatcher_metric_ckey(
580            MetricKind::Histogram,
581            DispatcherMetrics::send_latency_seconds_name(),
582            output_name,
583            &[],
584        );
585
586        // TODO: This API for querying the metrics really sucks... and we need something better.
587        let current_metrics = snapshotter.snapshot().into_hashmap();
588        let (_, _, events_sent) = current_metrics
589            .get(&events_sent_key)
590            .expect("should have events sent metric");
591        let (_, _, events_discarded) = current_metrics
592            .get(&events_discarded_key)
593            .expect("should have events discarded metric");
594        let (_, _, send_latency) = current_metrics
595            .get(&send_latency_key)
596            .expect("should have send latency metric");
597
598        let events_sent = match events_sent {
599            DebugValue::Counter(value) => *value,
600            _ => panic!("unexpected metric type for events sent"),
601        };
602
603        let events_discarded = match events_discarded {
604            DebugValue::Counter(value) => *value,
605            _ => panic!("unexpected metric type for events discarded"),
606        };
607
608        let send_latency = match send_latency {
609            DebugValue::Histogram(value) => value.clone(),
610            _ => panic!("unexpected metric type for send latency"),
611        };
612
613        (events_sent, events_discarded, send_latency)
614    }
615
616    #[tokio::test]
617    async fn default_output() {
618        // Create the dispatcher and wire up a sender to the default output.
619        let mut dispatcher = unbuffered_dispatcher::<SingleEvent<usize>>();
620
621        let (tx, mut rx) = mpsc::channel(1);
622        add_dispatcher_default_output(&mut dispatcher, [tx]);
623
624        // Create an item and roundtrip it through the dispatcher.
625        let input_item = 42.into();
626
627        dispatcher.dispatch(input_item).await.unwrap();
628
629        let output_item = rx.try_recv().expect("input item should have been dispatched");
630        assert_eq!(output_item, input_item);
631    }
632
633    #[tokio::test]
634    async fn named_output() {
635        // Create the dispatcher and wire up a sender to a named output.
636        let mut dispatcher = unbuffered_dispatcher::<SingleEvent<usize>>();
637
638        let output_name = "special";
639        let (tx, mut rx) = mpsc::channel(1);
640        add_dispatcher_named_output(&mut dispatcher, output_name, [tx]);
641
642        // Create an item and roundtrip it through the dispatcher.
643        let input_item = 42.into();
644
645        dispatcher.dispatch_named(output_name, input_item).await.unwrap();
646
647        let output_item = rx.try_recv().expect("input item should have been dispatched");
648        assert_eq!(output_item, input_item);
649    }
650
651    #[tokio::test]
652    async fn default_output_multiple_senders() {
653        // Create the dispatcher and wire up two senders to the default output.
654        let mut dispatcher = unbuffered_dispatcher::<SingleEvent<usize>>();
655
656        let (tx1, mut rx1) = mpsc::channel(1);
657        let (tx2, mut rx2) = mpsc::channel(1);
658        add_dispatcher_default_output(&mut dispatcher, [tx1, tx2]);
659
660        // Create an item and roundtrip it through the dispatcher.
661        let input_item = 42.into();
662
663        dispatcher.dispatch(input_item).await.unwrap();
664
665        let output_item1 = rx1.try_recv().expect("input item should have been dispatched");
666        let output_item2 = rx2.try_recv().expect("input item should have been dispatched");
667        assert_eq!(output_item1, input_item);
668        assert_eq!(output_item2, input_item);
669    }
670
671    #[tokio::test]
672    async fn named_output_multiple_senders() {
673        // Create the dispatcher and wire up two senders to a named output.
674        let mut dispatcher = unbuffered_dispatcher::<SingleEvent<usize>>();
675
676        let output_name = "special";
677        let (tx1, mut rx1) = mpsc::channel(1);
678        let (tx2, mut rx2) = mpsc::channel(1);
679        add_dispatcher_named_output(&mut dispatcher, output_name, [tx1, tx2]);
680
681        // Create an item and roundtrip it through the dispatcher.
682        let input_item = 42.into();
683
684        dispatcher.dispatch_named(output_name, input_item).await.unwrap();
685
686        let output_item1 = rx1.try_recv().expect("input item should have been dispatched");
687        let output_item2 = rx2.try_recv().expect("input item should have been dispatched");
688        assert_eq!(output_item1, input_item);
689        assert_eq!(output_item2, input_item);
690    }
691
692    #[tokio::test]
693    async fn default_output_not_set() {
694        // Create the dispatcher and try to dispatch an item without setting up a default output.
695        let dispatcher = unbuffered_dispatcher::<SingleEvent<()>>();
696
697        let result = dispatcher.dispatch(().into()).await;
698        assert!(result.is_err());
699    }
700
701    #[tokio::test]
702    async fn named_output_not_set() {
703        // Create the dispatcher and try to dispatch an event without setting up a named output.
704        let dispatcher = unbuffered_dispatcher::<SingleEvent<()>>();
705
706        let result = dispatcher.dispatch_named("non_existent", ().into()).await;
707        assert!(result.is_err());
708    }
709
710    #[tokio::test]
711    async fn default_output_buffered_partial() {
712        // Create the dispatcher and wire up a sender to the default output, using a bufferable type.
713        let mut dispatcher = buffered_dispatcher::<FixedUsizeVec<4>>();
714
715        let (tx, mut rx) = mpsc::channel(1);
716        add_dispatcher_default_output(&mut dispatcher, [tx]);
717
718        // Create an item and roundtrip it through the dispatcher.
719        let input_item = 42;
720
721        let mut buffered = dispatcher.buffered().unwrap();
722        buffered.push(input_item).await.unwrap();
723
724        let flushed_len = buffered.flush().await.unwrap();
725        assert_eq!(flushed_len, 1);
726
727        let output_item = rx.try_recv().expect("input item should have been dispatched");
728        assert_eq!(output_item.len(), 1);
729        assert_eq!(output_item[0], input_item);
730    }
731
732    #[tokio::test]
733    async fn named_output_buffered_partial() {
734        // Create the dispatcher and wire up a sender to a named output, using a bufferable type.
735        let mut dispatcher = buffered_dispatcher::<FixedUsizeVec<4>>();
736
737        let output_name = "buffered_partial";
738        let (tx, mut rx) = mpsc::channel(1);
739        add_dispatcher_named_output(&mut dispatcher, output_name, [tx]);
740
741        // Create an item and roundtrip it through the dispatcher.
742        let input_item = 42;
743
744        let mut buffered = dispatcher.buffered_named(output_name).unwrap();
745        buffered.push(input_item).await.unwrap();
746
747        let flushed_len = buffered.flush().await.unwrap();
748        assert_eq!(flushed_len, 1);
749
750        let output_item = rx.try_recv().expect("input item should have been dispatched");
751        assert_eq!(output_item.len(), 1);
752        assert_eq!(output_item[0], input_item);
753    }
754
755    #[tokio::test]
756    async fn default_output_buffered_overflow() {
757        // Create the dispatcher and wire up a sender to the default output, using a bufferable type.
758        let mut dispatcher = buffered_dispatcher::<FixedUsizeVec<4>>();
759
760        let (tx, mut rx) = mpsc::channel(2);
761        add_dispatcher_default_output(&mut dispatcher, [tx]);
762
763        // Create multiple items and roundtrip them through the dispatcher.
764        //
765        // We explicitly create more items than a single buffer can hold to exercise full buffers
766        // being flushed during push.
767        let input_items: Vec<usize> = vec![1, 2, 3, 4, 5, 6];
768
769        let mut buffered = dispatcher.buffered().unwrap();
770
771        for item in &input_items {
772            buffered.push(*item).await.unwrap();
773        }
774
775        let flushed_len = buffered.flush().await.unwrap();
776        assert_eq!(flushed_len, input_items.len());
777
778        let output_item1 = rx.try_recv().expect("input item should have been dispatched");
779        assert_eq!(output_item1.len(), 4);
780        assert_eq!(output_item1[0..4], input_items[0..4]);
781
782        let output_item2 = rx.try_recv().expect("input item should have been dispatched");
783        assert_eq!(output_item2.len(), 2);
784        assert_eq!(output_item2[0..2], input_items[4..6]);
785    }
786
787    #[tokio::test]
788    async fn named_output_buffered_overflow() {
789        // Create the dispatcher and wire up a sender to a named output, using a bufferable type.
790        let mut dispatcher = buffered_dispatcher::<FixedUsizeVec<4>>();
791
792        let output_name = "buffered_overflow";
793        let (tx, mut rx) = mpsc::channel(2);
794        add_dispatcher_named_output(&mut dispatcher, output_name, [tx]);
795
796        // Create multiple items and roundtrip them through the dispatcher.
797        //
798        // We explicitly create more items than a single buffer can hold to exercise full buffers
799        // being flushed during push.
800        let input_items: Vec<usize> = vec![1, 2, 3, 4, 5, 6];
801
802        let mut buffered = dispatcher.buffered_named(output_name).unwrap();
803
804        for item in &input_items {
805            buffered.push(*item).await.unwrap();
806        }
807
808        let flushed_len = buffered.flush().await.unwrap();
809        assert_eq!(flushed_len, input_items.len());
810
811        let output_item1 = rx.try_recv().expect("input item should have been dispatched");
812        assert_eq!(output_item1.len(), 4);
813        assert_eq!(output_item1[0..4], input_items[0..4]);
814
815        let output_item2 = rx.try_recv().expect("input item should have been dispatched");
816        assert_eq!(output_item2.len(), 2);
817        assert_eq!(output_item2[0..2], input_items[4..6]);
818    }
819
820    #[tokio::test]
821    async fn default_output_buffered_partial_multiple_senders() {
822        // Create the dispatcher and wire up two senders to the default output, using a bufferable type.
823        let mut dispatcher = buffered_dispatcher::<FixedUsizeVec<4>>();
824
825        let (tx1, mut rx1) = mpsc::channel(1);
826        let (tx2, mut rx2) = mpsc::channel(1);
827        add_dispatcher_default_output(&mut dispatcher, [tx1, tx2]);
828
829        // Create an item and roundtrip it through the dispatcher.
830        let input_item = 42;
831
832        let mut buffered = dispatcher.buffered().unwrap();
833        buffered.push(input_item).await.unwrap();
834
835        let flushed_len = buffered.flush().await.unwrap();
836        assert_eq!(flushed_len, 1);
837
838        let output_item1 = rx1.try_recv().expect("input item should have been dispatched");
839        assert_eq!(output_item1.len(), 1);
840        assert_eq!(output_item1[0], input_item);
841
842        let output_item2 = rx2.try_recv().expect("input item should have been dispatched");
843        assert_eq!(output_item2.len(), 1);
844        assert_eq!(output_item2[0], input_item);
845    }
846
847    #[tokio::test]
848    async fn named_output_buffered_partial_multiple_senders() {
849        // Create the dispatcher and wire up two senders to a named output, using a bufferable type.
850        let mut dispatcher = buffered_dispatcher::<FixedUsizeVec<4>>();
851
852        let output_name = "buffered_partial";
853        let (tx1, mut rx1) = mpsc::channel(1);
854        let (tx2, mut rx2) = mpsc::channel(1);
855        add_dispatcher_named_output(&mut dispatcher, output_name, [tx1, tx2]);
856
857        // Create an item and roundtrip it through the dispatcher.
858        let input_item = 42;
859
860        let mut buffered = dispatcher.buffered_named(output_name).unwrap();
861        buffered.push(input_item).await.unwrap();
862
863        let flushed_len = buffered.flush().await.unwrap();
864        assert_eq!(flushed_len, 1);
865
866        let output_item1 = rx1.try_recv().expect("input item should have been dispatched");
867        assert_eq!(output_item1.len(), 1);
868        assert_eq!(output_item1[0], input_item);
869
870        let output_item2 = rx2.try_recv().expect("input item should have been dispatched");
871        assert_eq!(output_item2.len(), 1);
872        assert_eq!(output_item2[0], input_item);
873    }
874
875    #[tokio::test]
876    async fn default_output_no_senders() {
877        // Test that we can add a default output and dispatch to it even with no senders attached
878        let mut dispatcher = unbuffered_dispatcher::<SingleEvent<u32>>();
879
880        // Add default output but don't attach any senders
881        dispatcher
882            .add_output(OutputName::Default)
883            .expect("should be able to add default output");
884
885        // Should not panic when dispatching to output with no senders
886        let test_event = 42.into();
887        let result = dispatcher.dispatch(test_event).await;
888        assert!(
889            result.is_ok(),
890            "dispatch to default output with no senders should succeed"
891        );
892    }
893
894    #[tokio::test]
895    async fn named_output_no_senders() {
896        // Test that we can add a named output and dispatch to it even with no senders attached
897        let mut dispatcher = unbuffered_dispatcher::<SingleEvent<u32>>();
898
899        // Add named output but don't attach any senders
900        dispatcher
901            .add_output(OutputName::Given("errors".into()))
902            .expect("should be able to add named output");
903
904        // Should not panic when dispatching to output with no senders
905        let test_event = 42.into();
906        let result = dispatcher.dispatch_named("errors", test_event).await;
907        assert!(
908            result.is_ok(),
909            "dispatch to named output with no senders should succeed"
910        );
911    }
912
913    #[tokio::test]
914    async fn metrics_default_output_disconnected() {
915        let recorder = DebuggingRecorder::new();
916        let snapshotter = recorder.snapshotter();
917        let dispatcher = metrics::with_local_recorder(&recorder, || {
918            let mut dispatcher = buffered_dispatcher::<FixedUsizeVec<4>>();
919            dispatcher
920                .add_output(OutputName::Default)
921                .expect("should not fail to add default output");
922            dispatcher
923        });
924
925        // Send an item with an item count of 1, and make sure we can receive it, and that we update our metrics accordingly:
926        let mut single_item = FixedUsizeVec::<4>::default();
927        assert_eq!(None, single_item.try_push(42));
928        let single_item_item_count = single_item.item_count() as u64;
929
930        dispatcher
931            .dispatch(single_item.clone())
932            .await
933            .expect("should not fail to dispatch");
934
935        let (events_sent, events_discarded, send_latencies) = get_output_metrics(&snapshotter, "_default");
936        assert_eq!(events_sent, 0);
937        assert_eq!(events_discarded, single_item_item_count);
938        assert!(send_latencies.is_empty());
939
940        // Now send an item with an item count of 3, and make sure we can receive it, and that we update our metrics accordingly:
941        let mut multiple_items = FixedUsizeVec::<4>::default();
942        assert_eq!(None, multiple_items.try_push(42));
943        assert_eq!(None, multiple_items.try_push(12345));
944        assert_eq!(None, multiple_items.try_push(1337));
945        let multiple_items_item_count = multiple_items.item_count() as u64;
946
947        dispatcher
948            .dispatch(multiple_items.clone())
949            .await
950            .expect("should not fail to dispatch");
951
952        let (events_sent, events_discarded, send_latencies) = get_output_metrics(&snapshotter, "_default");
953        assert_eq!(events_sent, 0);
954        assert_eq!(events_discarded, multiple_items_item_count);
955        assert!(send_latencies.is_empty());
956    }
957
958    #[tokio::test]
959    async fn metrics_named_output_disconnected() {
960        let output_name = "some_output";
961
962        let recorder = DebuggingRecorder::new();
963        let snapshotter = recorder.snapshotter();
964        let dispatcher = metrics::with_local_recorder(&recorder, || {
965            let mut dispatcher = buffered_dispatcher::<FixedUsizeVec<4>>();
966            dispatcher
967                .add_output(OutputName::Given(output_name.into()))
968                .expect("should not fail to add named output");
969            dispatcher
970        });
971
972        // Send an item with an item count of 1, and make sure we can receive it, and that we update our metrics accordingly:
973        let mut single_item = FixedUsizeVec::<4>::default();
974        assert_eq!(None, single_item.try_push(42));
975        let single_item_item_count = single_item.item_count() as u64;
976
977        dispatcher
978            .dispatch_named(output_name, single_item.clone())
979            .await
980            .expect("should not fail to dispatch");
981
982        let (events_sent, events_discarded, send_latencies) = get_output_metrics(&snapshotter, output_name);
983        assert_eq!(events_sent, 0);
984        assert_eq!(events_discarded, single_item_item_count);
985        assert!(send_latencies.is_empty());
986
987        // Now send an item with an item count of 3, and make sure we can receive it, and that we update our metrics accordingly:
988        let mut multiple_items = FixedUsizeVec::<4>::default();
989        assert_eq!(None, multiple_items.try_push(42));
990        assert_eq!(None, multiple_items.try_push(12345));
991        assert_eq!(None, multiple_items.try_push(1337));
992        let multiple_items_item_count = multiple_items.item_count() as u64;
993
994        dispatcher
995            .dispatch_named(output_name, multiple_items.clone())
996            .await
997            .expect("should not fail to dispatch");
998
999        let (events_sent, events_discarded, send_latencies) = get_output_metrics(&snapshotter, output_name);
1000        assert_eq!(events_sent, 0);
1001        assert_eq!(events_discarded, multiple_items_item_count);
1002        assert!(send_latencies.is_empty());
1003    }
1004
1005    #[tokio::test]
1006    async fn is_default_output_connected_behavior() {
1007        let mut dispatcher = unbuffered_dispatcher::<SingleEvent<u32>>();
1008
1009        // Initially, no default output exists - should return false
1010        assert!(
1011            !dispatcher.is_default_output_connected(),
1012            "should return false when no default output exists"
1013        );
1014
1015        // Add default output but no senders - should return false
1016        dispatcher
1017            .add_output(OutputName::Default)
1018            .expect("should be able to add default output");
1019        assert!(
1020            !dispatcher.is_default_output_connected(),
1021            "should return false when default output exists but has no senders"
1022        );
1023
1024        // Add a sender to the default output - should return true
1025        let (tx, _rx) = mpsc::channel(1);
1026        dispatcher
1027            .attach_sender_to_output(&OutputName::Default, tx)
1028            .expect("should be able to attach sender");
1029        assert!(
1030            dispatcher.is_default_output_connected(),
1031            "should return true when default output has senders attached"
1032        );
1033    }
1034
1035    #[tokio::test]
1036    async fn is_named_output_connected_behavior() {
1037        let mut dispatcher = unbuffered_dispatcher::<SingleEvent<u32>>();
1038        let output_name = "test_output";
1039
1040        // Initially, no named output exists - should return false
1041        assert!(
1042            !dispatcher.is_named_output_connected(output_name),
1043            "should return false when named output doesn't exist"
1044        );
1045
1046        // Add named output but no senders - should return false
1047        dispatcher
1048            .add_output(OutputName::Given(output_name.into()))
1049            .expect("should be able to add named output");
1050        assert!(
1051            !dispatcher.is_named_output_connected(output_name),
1052            "should return false when named output exists but has no senders"
1053        );
1054
1055        // Add a sender to the named output - should return true
1056        let (tx, _rx) = mpsc::channel(1);
1057        dispatcher
1058            .attach_sender_to_output(&OutputName::Given(output_name.into()), tx)
1059            .expect("should be able to attach sender");
1060        assert!(
1061            dispatcher.is_named_output_connected(output_name),
1062            "should return true when named output has senders attached"
1063        );
1064
1065        // Test with a different output name that doesn't exist - should return false
1066        assert!(
1067            !dispatcher.is_named_output_connected("nonexistent_output"),
1068            "should return false for nonexistent output"
1069        );
1070    }
1071
1072    #[tokio::test]
1073    async fn default_output_dispatch_one() {
1074        // Dispatch a single item to the default output and confirm it arrives wrapped in a one-element buffer.
1075        let mut dispatcher = buffered_dispatcher::<FixedUsizeVec<4>>();
1076
1077        let (tx, mut rx) = mpsc::channel(1);
1078        add_dispatcher_default_output(&mut dispatcher, [tx]);
1079
1080        let input_item = 42;
1081
1082        dispatcher.dispatch_one(input_item).await.unwrap();
1083
1084        let output_item = rx.try_recv().expect("input item should have been dispatched");
1085        assert_eq!(output_item.len(), 1);
1086        assert_eq!(output_item[0], input_item);
1087    }
1088
1089    #[tokio::test]
1090    async fn named_output_dispatch_one() {
1091        // Same as above but for a named output.
1092        let mut dispatcher = buffered_dispatcher::<FixedUsizeVec<4>>();
1093
1094        let output_name = "single";
1095        let (tx, mut rx) = mpsc::channel(1);
1096        add_dispatcher_named_output(&mut dispatcher, output_name, [tx]);
1097
1098        let input_item = 42;
1099
1100        dispatcher.dispatch_one_named(output_name, input_item).await.unwrap();
1101
1102        let output_item = rx.try_recv().expect("input item should have been dispatched");
1103        assert_eq!(output_item.len(), 1);
1104        assert_eq!(output_item[0], input_item);
1105    }
1106
1107    #[tokio::test]
1108    async fn default_output_dispatch_one_not_set() {
1109        // dispatch_one without a default output configured should error.
1110        let dispatcher = buffered_dispatcher::<FixedUsizeVec<4>>();
1111
1112        let result = dispatcher.dispatch_one(42).await;
1113        assert!(result.is_err());
1114    }
1115
1116    #[tokio::test]
1117    async fn named_output_dispatch_one_not_set() {
1118        // dispatch_one_named on an unknown output should error.
1119        let dispatcher = buffered_dispatcher::<FixedUsizeVec<4>>();
1120
1121        let result = dispatcher.dispatch_one_named("nonexistent", 42).await;
1122        assert!(result.is_err());
1123    }
1124}