saluki_core/topology/interconnect/
consumer.rs

1use saluki_metrics::{static_metrics, Counter, Histogram};
2use tokio::sync::mpsc;
3
4use super::Dispatchable;
5use crate::components::ComponentContext;
6
7#[static_metrics(prefix = component, labels(component_id, component_type))]
8#[derive(Clone)]
9struct ConsumerMetrics {
10    events_received_total: Counter,
11    #[metric(level = trace)]
12    events_received_size: Histogram,
13}
14
15impl ConsumerMetrics {
16    fn from_component_context(context: ComponentContext) -> Self {
17        Self::new(context.component_id(), context.component_type().as_str())
18    }
19}
20
21/// A stream of items sent to a component.
22///
23/// This represents the receiving end of a component interconnect, where the sending end is [`Dispatcher<T>`][super::Dispatcher].
24pub struct Consumer<T> {
25    inner: mpsc::Receiver<T>,
26    metrics: ConsumerMetrics,
27}
28
29impl<T> Consumer<T>
30where
31    T: Dispatchable,
32{
33    /// Create a new `Consumer` for the given component context and inner receiver.
34    pub fn new(context: ComponentContext, inner: mpsc::Receiver<T>) -> Self {
35        Self {
36            inner,
37            metrics: ConsumerMetrics::from_component_context(context),
38        }
39    }
40
41    /// Gets the next item in the stream.
42    ///
43    /// If the component (or components) connected to this consumer have stopped, `None` is returned.
44    pub async fn next(&mut self) -> Option<T> {
45        match self.inner.recv().await {
46            Some(item) => {
47                self.metrics.events_received_total().increment(item.item_count() as u64);
48                self.metrics.events_received_size().record(item.item_count() as f64);
49                Some(item)
50            }
51            None => None,
52        }
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use metrics::{Key, Label};
59    use metrics_util::{
60        debugging::{DebugValue, DebuggingRecorder},
61        CompositeKey, MetricKind,
62    };
63    use ordered_float::OrderedFloat;
64
65    use super::*;
66
67    #[derive(Clone, Debug, Eq, PartialEq)]
68    struct DispatchableEvent<T> {
69        item_count: usize,
70        data: T,
71    }
72
73    impl<T: Clone> DispatchableEvent<T> {
74        fn new(data: T) -> Self {
75            Self { item_count: 1, data }
76        }
77
78        fn with_item_count(item_count: usize, data: T) -> Self {
79            Self { item_count, data }
80        }
81    }
82
83    impl<T: Clone> Dispatchable for DispatchableEvent<T> {
84        fn item_count(&self) -> usize {
85            self.item_count
86        }
87    }
88
89    fn create_consumer<T: Clone>(
90        channel_size: usize,
91    ) -> (Consumer<DispatchableEvent<T>>, mpsc::Sender<DispatchableEvent<T>>) {
92        let component_context = ComponentContext::test_source("consumer_test");
93
94        let (tx, rx) = mpsc::channel(channel_size);
95        let consumer = Consumer::new(component_context, rx);
96
97        (consumer, tx)
98    }
99
100    fn get_consumer_metric_composite_key(kind: MetricKind, name: &'static str) -> CompositeKey {
101        // We build the labels according to what we'll generate when calling `create_consumer`:
102        static LABELS: &[Label] = &[
103            Label::from_static_parts("component_id", "consumer_test"),
104            Label::from_static_parts("component_type", "source"),
105        ];
106        let key = Key::from_static_parts(name, LABELS);
107        CompositeKey::new(kind, key)
108    }
109
110    #[tokio::test]
111    async fn next() {
112        let (mut consumer, tx) = create_consumer(1);
113
114        // Send an item, and make sure we can receive it:
115        let input_item = DispatchableEvent::new("hello world");
116        tx.send(input_item.clone()).await.expect("should not fail to send item");
117
118        let output_item = consumer.next().await.expect("should receive item");
119        assert_eq!(output_item, input_item);
120
121        // Now drop the sender, which should close the consumer:
122        drop(tx);
123
124        assert!(consumer.next().await.is_none());
125    }
126
127    #[tokio::test]
128    async fn metrics() {
129        let events_received_key =
130            get_consumer_metric_composite_key(MetricKind::Counter, ConsumerMetrics::events_received_total_name());
131        let events_received_size_key =
132            get_consumer_metric_composite_key(MetricKind::Histogram, ConsumerMetrics::events_received_size_name());
133
134        let recorder = DebuggingRecorder::new();
135        let snapshotter = recorder.snapshotter();
136        let (mut consumer, tx) = metrics::with_local_recorder(&recorder, || create_consumer(1));
137
138        // Send an item with an item count of 1, and make sure we can receive it, and that we update our metrics accordingly:
139        let single_item = DispatchableEvent::new("single item");
140        tx.send(single_item.clone())
141            .await
142            .expect("should not fail to send item");
143
144        let output_item = consumer.next().await.expect("should receive item");
145        assert_eq!(output_item, single_item);
146
147        // TODO: This API for querying the metrics really sucks... and we need something better.
148        let current_metrics = snapshotter.snapshot().into_hashmap();
149        let (_, _, events_received) = current_metrics
150            .get(&events_received_key)
151            .expect("should have events received metric");
152        let (_, _, events_received_size) = current_metrics
153            .get(&events_received_size_key)
154            .expect("should have events received size metric");
155        assert_eq!(events_received, &DebugValue::Counter(1));
156        let expected_sizes = vec![OrderedFloat(1.0)];
157        assert_eq!(events_received_size, &DebugValue::Histogram(expected_sizes));
158
159        // Now send an item with an item count of 42, and make sure we can receive it, and that we update our metrics accordingly:
160        let multiple_items = DispatchableEvent::with_item_count(42, "multiple_items");
161        tx.send(multiple_items.clone())
162            .await
163            .expect("should not fail to send item");
164
165        let output_item = consumer.next().await.expect("should receive item");
166        assert_eq!(output_item, multiple_items);
167
168        // TODO: This API for querying the metrics really sucks... and we need something better.
169        let current_metrics = snapshotter.snapshot().into_hashmap();
170        let (_, _, events_received) = current_metrics
171            .get(&events_received_key)
172            .expect("should have events received metric");
173        let (_, _, events_received_size) = current_metrics
174            .get(&events_received_size_key)
175            .expect("should have events received size metric");
176        assert_eq!(events_received, &DebugValue::Counter(42));
177
178        let expected_sizes = vec![OrderedFloat(42.0)];
179        assert_eq!(events_received_size, &DebugValue::Histogram(expected_sizes));
180    }
181}