use super::EncodedChunk;
use crate::metrics::{ComponentLabels, MetricsError, QueueMetrics};
use std::sync::Arc;
use tokio::sync::mpsc;
#[derive(Debug)]
pub struct ChunkSendError(pub EncodedChunk);
#[derive(Clone, Debug)]
pub struct ShardQueues {
senders: Vec<mpsc::Sender<EncodedChunk>>,
capacity: usize,
metrics: Option<Arc<Vec<QueueMetrics>>>,
}
impl ShardQueues {
#[must_use]
pub fn num_shards(&self) -> usize {
self.senders.len()
}
#[must_use]
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn try_send(&self, shard: usize, chunk: EncodedChunk) -> Result<(), ChunkSendError> {
let (result, live) = match self.senders[shard].try_send(chunk) {
Ok(()) => (Ok(()), true),
Err(mpsc::error::TrySendError::Full(c)) => {
if let Some(m) = &self.metrics {
m[shard].full_events(1);
}
(Err(ChunkSendError(c)), true)
}
Err(mpsc::error::TrySendError::Closed(c)) => (Err(ChunkSendError(c)), false),
};
if let Some(m) = self.metrics.as_ref().filter(|_| live) {
m[shard].set_depth(self.capacity - self.senders[shard].capacity());
}
result
}
pub(crate) fn attach_metrics(&mut self, labels: &ComponentLabels) -> Result<(), MetricsError> {
let metrics = (0..self.senders.len())
.map(|i| {
QueueMetrics::try_new(labels, &format!("chain->sink/shard-{i}"), self.capacity)
})
.collect::<Result<_, _>>()?;
self.metrics = Some(Arc::new(metrics));
Ok(())
}
#[must_use]
pub fn all_below(&self, ratio: f64) -> bool {
let threshold = (self.capacity as f64 * ratio) as usize;
self.senders
.iter()
.all(|s| self.capacity - s.capacity() <= threshold)
}
}
#[must_use]
pub fn shard_queues(
num_shards: usize,
capacity: usize,
) -> (ShardQueues, Vec<mpsc::Receiver<EncodedChunk>>) {
assert!(num_shards > 0, "a sink needs at least one shard");
assert!(capacity > 0, "shard queues need non-zero capacity");
let (senders, receivers): (Vec<_>, Vec<_>) =
(0..num_shards).map(|_| mpsc::channel(capacity)).unzip();
(
ShardQueues {
senders,
capacity,
metrics: None,
},
receivers,
)
}
#[cfg(all(test, not(loom)))]
mod tests {
use super::*;
use bytes::Bytes;
fn chunk() -> EncodedChunk {
EncodedChunk {
oldest_ingest: std::time::Instant::now(),
oldest_event_ms: 0,
frame: Bytes::from_static(b"x"),
rows: 1,
acks: crate::checkpoint::AckSet::new(),
}
}
#[test]
fn try_send_never_blocks_and_returns_the_chunk_when_full() {
let (q, mut rx) = shard_queues(1, 2);
assert!(q.try_send(0, chunk()).is_ok());
assert!(q.try_send(0, chunk()).is_ok());
let ChunkSendError(returned) = q.try_send(0, chunk()).unwrap_err();
assert_eq!(returned.rows, 1);
assert!(rx[0].try_recv().is_ok());
assert!(q.try_send(0, chunk()).is_ok(), "capacity freed");
let _ = rx;
}
#[test]
fn dropping_a_receiver_with_queued_chunks_fails_their_acks() {
use crate::checkpoint::{AckRef, AckStatus};
let (q, rx) = shard_queues(1, 4);
let (ack, ack_rx) = AckRef::test_pair();
let mut c = chunk();
c.acks = vec![ack.clone()].into();
drop(ack);
q.try_send(0, c).expect("queued");
drop(rx); assert_eq!(
ack_rx.try_recv().expect("resolved").status,
AckStatus::Failed,
"chunks lost in a dropped queue must fail their batches"
);
}
#[test]
fn closed_queue_hands_the_chunk_back() {
let (q, rx) = shard_queues(1, 1);
drop(rx);
assert!(q.try_send(0, chunk()).is_err());
}
#[test]
fn all_below_reflects_fill_ratio() {
let (q, _rx) = shard_queues(2, 4);
assert!(q.all_below(0.5));
q.try_send(0, chunk()).unwrap();
q.try_send(0, chunk()).unwrap();
q.try_send(0, chunk()).unwrap();
assert!(!q.all_below(0.5), "shard 0 is 75% full");
}
#[test]
fn attached_metrics_emit_the_documented_queue_family() {
use metrics_exporter_prometheus::PrometheusBuilder;
let recorder = PrometheusBuilder::new().build_recorder();
let handle = recorder.handle();
metrics::with_local_recorder(&recorder, || {
let (mut q, rx) = shard_queues(1, 2);
q.attach_metrics(&ComponentLabels::new(
"orders",
"queue-family-test",
"clickhouse",
))
.expect("free series");
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"); });
let rendered = handle.render();
let series = r#"{pipeline="orders",component="queue-family-test",component_type="clickhouse",queue="chain->sink/shard-0"}"#;
for needle in [
format!("spate_queue_capacity{series} 2"),
format!("spate_queue_full_events_total{series} 1"),
format!("spate_queue_depth{series} 2"),
] {
assert!(
rendered.contains(&needle),
"rendered output missing `{needle}`:\n{rendered}"
);
}
}
}