1use super::{EncodedChunk, FrameCodec, FrameCodecFactory};
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 codec: Option<FrameCodecFactory>,
28}
29
30impl ShardQueues {
31 #[must_use]
38 pub fn with_frame_codec(mut self, codec: FrameCodecFactory) -> Self {
39 self.codec = Some(codec);
40 self
41 }
42
43 pub(crate) fn new_frame_codec(&self) -> Option<Box<dyn FrameCodec>> {
46 self.codec.as_ref().map(FrameCodecFactory::make)
47 }
48
49 #[must_use]
51 pub fn num_shards(&self) -> usize {
52 self.senders.len()
53 }
54
55 #[must_use]
57 pub fn capacity(&self) -> usize {
58 self.capacity
59 }
60
61 pub fn try_send(&self, shard: usize, chunk: EncodedChunk) -> Result<(), ChunkSendError> {
65 let (result, live) = match self.senders[shard].try_send(chunk) {
66 Ok(()) => (Ok(()), true),
67 Err(mpsc::error::TrySendError::Full(c)) => {
68 if let Some(m) = &self.metrics {
71 m[shard].full_events(1);
72 }
73 (Err(ChunkSendError(c)), true)
74 }
75 Err(mpsc::error::TrySendError::Closed(c)) => (Err(ChunkSendError(c)), false),
78 };
79 if let Some(m) = self.metrics.as_ref().filter(|_| live) {
82 m[shard].set_depth(self.capacity - self.senders[shard].capacity());
83 }
84 result
85 }
86
87 pub(crate) fn metrics(&self) -> Option<Arc<Vec<QueueMetrics>>> {
90 self.metrics.clone()
91 }
92
93 pub(crate) fn attach_metrics(&mut self, labels: &ComponentLabels) -> Result<(), MetricsError> {
103 let metrics = (0..self.senders.len())
104 .map(|i| {
105 QueueMetrics::try_new(labels, &format!("chain->sink/shard-{i}"), self.capacity)
106 })
107 .collect::<Result<_, _>>()?;
108 self.metrics = Some(Arc::new(metrics));
109 Ok(())
110 }
111
112 #[must_use]
115 pub fn all_below(&self, ratio: f64) -> bool {
116 let threshold = (self.capacity as f64 * ratio) as usize;
117 self.senders
118 .iter()
119 .all(|s| self.capacity - s.capacity() <= threshold)
120 }
121}
122
123#[must_use]
126pub fn shard_queues(
127 num_shards: usize,
128 capacity: usize,
129) -> (ShardQueues, Vec<mpsc::Receiver<EncodedChunk>>) {
130 assert!(num_shards > 0, "a sink needs at least one shard");
131 assert!(capacity > 0, "shard queues need non-zero capacity");
132 let (senders, receivers): (Vec<_>, Vec<_>) =
133 (0..num_shards).map(|_| mpsc::channel(capacity)).unzip();
134 (
135 ShardQueues {
136 senders,
137 capacity,
138 metrics: None,
139 codec: None,
140 },
141 receivers,
142 )
143}
144
145#[cfg(all(test, not(loom)))]
146mod tests {
147 use super::*;
148 use bytes::Bytes;
149
150 fn chunk() -> EncodedChunk {
151 EncodedChunk {
152 oldest_ingest: std::time::Instant::now(),
153 oldest_event_ms: 0,
154 frame: Bytes::from_static(b"x"),
155 raw_len: 1,
156 rows: 1,
157 acks: crate::checkpoint::AckSet::new(),
158 }
159 }
160
161 #[test]
162 fn try_send_never_blocks_and_returns_the_chunk_when_full() {
163 let (q, mut rx) = shard_queues(1, 2);
164 assert!(q.try_send(0, chunk()).is_ok());
165 assert!(q.try_send(0, chunk()).is_ok());
166 let ChunkSendError(returned) = q.try_send(0, chunk()).unwrap_err();
167 assert_eq!(returned.rows, 1);
168 assert!(rx[0].try_recv().is_ok());
169 assert!(q.try_send(0, chunk()).is_ok(), "capacity freed");
170 let _ = rx;
171 }
172
173 #[test]
174 fn dropping_a_receiver_with_queued_chunks_fails_their_acks() {
175 use crate::checkpoint::{AckRef, AckStatus};
176 let (q, rx) = shard_queues(1, 4);
177 let (ack, ack_rx) = AckRef::test_pair();
178 let mut c = chunk();
179 c.acks = vec![ack.clone()].into();
180 drop(ack);
181 q.try_send(0, c).expect("queued");
182 drop(rx); assert_eq!(
184 ack_rx.try_recv().expect("resolved").status,
185 AckStatus::Failed,
186 "chunks lost in a dropped queue must fail their batches"
187 );
188 }
189
190 #[test]
191 fn closed_queue_hands_the_chunk_back() {
192 let (q, rx) = shard_queues(1, 1);
193 drop(rx);
194 assert!(q.try_send(0, chunk()).is_err());
195 }
196
197 #[test]
198 fn all_below_reflects_fill_ratio() {
199 let (q, _rx) = shard_queues(2, 4);
200 assert!(q.all_below(0.5));
201 q.try_send(0, chunk()).unwrap();
202 q.try_send(0, chunk()).unwrap();
203 q.try_send(0, chunk()).unwrap();
204 assert!(!q.all_below(0.5), "shard 0 is 75% full");
205 }
206
207 #[test]
208 fn attached_metrics_emit_the_documented_queue_family() {
209 use metrics_exporter_prometheus::PrometheusBuilder;
210
211 let recorder = PrometheusBuilder::new().build_recorder();
212 let handle = recorder.handle();
213 metrics::with_local_recorder(&recorder, || {
214 let (mut q, rx) = shard_queues(1, 2);
215 q.attach_metrics(&ComponentLabels::new(
216 "orders",
217 "queue-family-test",
218 "clickhouse",
219 ))
220 .expect("free series");
221 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"); });
227 let rendered = handle.render();
228
229 let series = r#"{pipeline="orders",component="queue-family-test",component_type="clickhouse",queue="chain->sink/shard-0"}"#;
230 for needle in [
231 format!("spate_queue_capacity{series} 2"),
232 format!("spate_queue_full_events_total{series} 1"),
234 format!("spate_queue_depth{series} 2"),
236 ] {
237 assert!(
238 rendered.contains(&needle),
239 "rendered output missing `{needle}`:\n{rendered}"
240 );
241 }
242 }
243}