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