1use super::EncodedChunk;
8use crate::metrics::{ComponentLabels, MetricsError, QueueMetrics};
9use std::sync::Arc;
10use tokio::sync::mpsc;
11
12#[derive(Debug)]
15pub struct ChunkSendError(pub EncodedChunk);
16
17#[derive(Clone, Debug)]
19pub struct ShardQueues {
20 senders: Vec<mpsc::Sender<EncodedChunk>>,
21 capacity: usize,
22 metrics: Option<Arc<Vec<QueueMetrics>>>,
26}
27
28impl ShardQueues {
29 #[must_use]
31 pub fn num_shards(&self) -> usize {
32 self.senders.len()
33 }
34
35 #[must_use]
37 pub fn capacity(&self) -> usize {
38 self.capacity
39 }
40
41 pub fn try_send(&self, shard: usize, chunk: EncodedChunk) -> Result<(), ChunkSendError> {
45 let (result, live) = match self.senders[shard].try_send(chunk) {
46 Ok(()) => (Ok(()), true),
47 Err(mpsc::error::TrySendError::Full(c)) => {
48 if let Some(m) = &self.metrics {
51 m[shard].full_events(1);
52 }
53 (Err(ChunkSendError(c)), true)
54 }
55 Err(mpsc::error::TrySendError::Closed(c)) => (Err(ChunkSendError(c)), false),
58 };
59 if let Some(m) = self.metrics.as_ref().filter(|_| live) {
62 m[shard].set_depth(self.capacity - self.senders[shard].capacity());
63 }
64 result
65 }
66
67 pub(crate) fn attach_metrics(&mut self, labels: &ComponentLabels) -> Result<(), MetricsError> {
77 let metrics = (0..self.senders.len())
78 .map(|i| {
79 QueueMetrics::try_new(labels, &format!("chain->sink/shard-{i}"), self.capacity)
80 })
81 .collect::<Result<_, _>>()?;
82 self.metrics = Some(Arc::new(metrics));
83 Ok(())
84 }
85
86 #[must_use]
89 pub fn all_below(&self, ratio: f64) -> bool {
90 let threshold = (self.capacity as f64 * ratio) as usize;
91 self.senders
92 .iter()
93 .all(|s| self.capacity - s.capacity() <= threshold)
94 }
95}
96
97#[must_use]
100pub fn shard_queues(
101 num_shards: usize,
102 capacity: usize,
103) -> (ShardQueues, Vec<mpsc::Receiver<EncodedChunk>>) {
104 assert!(num_shards > 0, "a sink needs at least one shard");
105 assert!(capacity > 0, "shard queues need non-zero capacity");
106 let (senders, receivers): (Vec<_>, Vec<_>) =
107 (0..num_shards).map(|_| mpsc::channel(capacity)).unzip();
108 (
109 ShardQueues {
110 senders,
111 capacity,
112 metrics: None,
113 },
114 receivers,
115 )
116}
117
118#[cfg(all(test, not(loom)))]
119mod tests {
120 use super::*;
121 use bytes::Bytes;
122
123 fn chunk() -> EncodedChunk {
124 EncodedChunk {
125 oldest_ingest: std::time::Instant::now(),
126 oldest_event_ms: 0,
127 frame: Bytes::from_static(b"x"),
128 rows: 1,
129 acks: crate::checkpoint::AckSet::new(),
130 }
131 }
132
133 #[test]
134 fn try_send_never_blocks_and_returns_the_chunk_when_full() {
135 let (q, mut rx) = shard_queues(1, 2);
136 assert!(q.try_send(0, chunk()).is_ok());
137 assert!(q.try_send(0, chunk()).is_ok());
138 let ChunkSendError(returned) = q.try_send(0, chunk()).unwrap_err();
139 assert_eq!(returned.rows, 1);
140 assert!(rx[0].try_recv().is_ok());
141 assert!(q.try_send(0, chunk()).is_ok(), "capacity freed");
142 let _ = rx;
143 }
144
145 #[test]
146 fn dropping_a_receiver_with_queued_chunks_fails_their_acks() {
147 use crate::checkpoint::{AckRef, AckStatus};
148 let (q, rx) = shard_queues(1, 4);
149 let (ack, ack_rx) = AckRef::test_pair();
150 let mut c = chunk();
151 c.acks = vec![ack.clone()].into();
152 drop(ack);
153 q.try_send(0, c).expect("queued");
154 drop(rx); assert_eq!(
156 ack_rx.try_recv().expect("resolved").status,
157 AckStatus::Failed,
158 "chunks lost in a dropped queue must fail their batches"
159 );
160 }
161
162 #[test]
163 fn closed_queue_hands_the_chunk_back() {
164 let (q, rx) = shard_queues(1, 1);
165 drop(rx);
166 assert!(q.try_send(0, chunk()).is_err());
167 }
168
169 #[test]
170 fn all_below_reflects_fill_ratio() {
171 let (q, _rx) = shard_queues(2, 4);
172 assert!(q.all_below(0.5));
173 q.try_send(0, chunk()).unwrap();
174 q.try_send(0, chunk()).unwrap();
175 q.try_send(0, chunk()).unwrap();
176 assert!(!q.all_below(0.5), "shard 0 is 75% full");
177 }
178
179 #[test]
180 fn attached_metrics_emit_the_documented_queue_family() {
181 use metrics_exporter_prometheus::PrometheusBuilder;
182
183 let recorder = PrometheusBuilder::new().build_recorder();
184 let handle = recorder.handle();
185 metrics::with_local_recorder(&recorder, || {
186 let (mut q, rx) = shard_queues(1, 2);
187 q.attach_metrics(&ComponentLabels::new(
188 "orders",
189 "queue-family-test",
190 "clickhouse",
191 ))
192 .expect("free series");
193 q.try_send(0, chunk()).expect("first send fits"); q.try_send(0, chunk()).expect("second send fills"); q.try_send(0, chunk()).expect_err("full"); drop(rx); q.try_send(0, chunk()).expect_err("closed"); });
199 let rendered = handle.render();
200
201 let series = r#"{pipeline="orders",component="queue-family-test",component_type="clickhouse",queue="chain->sink/shard-0"}"#;
202 for needle in [
203 format!("spate_queue_capacity{series} 2"),
204 format!("spate_queue_full_events_total{series} 1"),
206 format!("spate_queue_depth{series} 2"),
208 ] {
209 assert!(
210 rendered.contains(&needle),
211 "rendered output missing `{needle}`:\n{rendered}"
212 );
213 }
214 }
215}