use crate::pipeline::{MetricEvent, MetricPipeline, MetricType};
use alice_queue::{AliceQueue, Message};
pub const METRIC_PAYLOAD_SIZE: usize = 17;
#[inline]
pub fn encode_metric_payload(event: &MetricEvent) -> [u8; METRIC_PAYLOAD_SIZE] {
let mut buf = [0u8; METRIC_PAYLOAD_SIZE];
buf[0] = match event.metric_type {
MetricType::Counter => 0,
MetricType::Gauge => 1,
MetricType::Histogram => 2,
MetricType::Unique => 3,
};
buf[1..9].copy_from_slice(&event.name_hash.to_le_bytes());
buf[9..17].copy_from_slice(&event.value.to_le_bytes());
buf
}
#[inline]
pub fn parse_metric_event(msg: &Message) -> Option<MetricEvent> {
let payload = &msg.payload;
if payload.len() < METRIC_PAYLOAD_SIZE {
return None;
}
let metric_type = match payload[0] {
0 => MetricType::Counter,
1 => MetricType::Gauge,
2 => MetricType::Histogram,
3 => MetricType::Unique,
_ => return None,
};
let name_hash = u64::from_le_bytes([
payload[1], payload[2], payload[3], payload[4], payload[5], payload[6], payload[7],
payload[8],
]);
let value = f64::from_le_bytes([
payload[9],
payload[10],
payload[11],
payload[12],
payload[13],
payload[14],
payload[15],
payload[16],
]);
Some(MetricEvent {
name_hash,
metric_type,
value,
timestamp: 0,
})
}
pub struct QueueConsumerPipeline<const SLOTS: usize, const PIPE_QUEUE: usize, const Q: usize> {
pub queue: AliceQueue<Q>,
pub pipeline: MetricPipeline<SLOTS, PIPE_QUEUE>,
consumed: u64,
parse_errors: u64,
}
impl<const SLOTS: usize, const PIPE_QUEUE: usize, const Q: usize>
QueueConsumerPipeline<SLOTS, PIPE_QUEUE, Q>
{
pub fn new(alpha: f64) -> Self {
Self {
queue: AliceQueue::new(),
pipeline: MetricPipeline::new(alpha),
consumed: 0,
parse_errors: 0,
}
}
pub fn drain(&mut self) -> usize {
let mut count = 0;
while let Some((msg, _gap)) = self.queue.dequeue() {
self.consumed += 1;
if let Some(event) = parse_metric_event(&msg) {
self.pipeline.submit(event);
count += 1;
} else {
self.parse_errors += 1;
}
}
if count > 0 {
self.pipeline.flush();
}
count
}
pub fn drain_batch(&mut self, max: usize) -> usize {
let mut count = 0;
for _ in 0..max {
match self.queue.dequeue() {
Some((msg, _gap)) => {
self.consumed += 1;
if let Some(event) = parse_metric_event(&msg) {
self.pipeline.submit(event);
count += 1;
} else {
self.parse_errors += 1;
}
}
None => break,
}
}
if count > 0 {
self.pipeline.flush();
}
count
}
#[inline]
pub fn total_consumed(&self) -> u64 {
self.consumed
}
#[inline]
pub fn total_parse_errors(&self) -> u64 {
self.parse_errors
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sketch::FnvHasher;
fn test_sender() -> [u8; 32] {
[0u8; 32]
}
#[test]
fn test_encode_decode_roundtrip() {
let events = [
MetricEvent::counter(0xDEADBEEF, 42.0),
MetricEvent::gauge(0xCAFEBABE, -3.14),
MetricEvent::histogram(123456, 99.9),
MetricEvent::unique(0xFF, 12345),
];
for original in &events {
let payload = encode_metric_payload(original);
assert_eq!(payload.len(), METRIC_PAYLOAD_SIZE);
let msg = Message::new(test_sender(), 1, payload.to_vec());
let decoded = parse_metric_event(&msg).unwrap();
assert_eq!(decoded.name_hash, original.name_hash);
assert_eq!(decoded.metric_type, original.metric_type);
assert_eq!(decoded.value, original.value);
}
}
#[test]
fn test_parse_invalid_payload() {
let msg = Message::new(test_sender(), 1, vec![0, 1, 2]);
assert!(parse_metric_event(&msg).is_none());
let mut payload = [0u8; METRIC_PAYLOAD_SIZE];
payload[0] = 255;
let msg = Message::new(test_sender(), 1, payload.to_vec());
assert!(parse_metric_event(&msg).is_none());
}
#[test]
fn test_consumer_pipeline_drain() {
let mut consumer = QueueConsumerPipeline::<64, 256, 128>::new(0.05);
let req_hash = FnvHasher::hash_bytes(b"http.requests");
for i in 0..10u64 {
let event = MetricEvent::counter(req_hash, 1.0);
let payload = encode_metric_payload(&event);
let msg = Message::new(test_sender(), i + 1, payload.to_vec());
consumer.queue.enqueue(msg).unwrap();
}
let processed = consumer.drain();
assert_eq!(processed, 10);
assert_eq!(consumer.total_consumed(), 10);
assert_eq!(consumer.total_parse_errors(), 0);
let slot = consumer.pipeline.get_slot(req_hash).unwrap();
assert_eq!(slot.counter, 10.0);
}
#[test]
fn test_consumer_pipeline_mixed_metrics() {
let mut consumer = QueueConsumerPipeline::<64, 256, 128>::new(0.05);
let counter_hash = FnvHasher::hash_bytes(b"requests");
let gauge_hash = FnvHasher::hash_bytes(b"cpu_usage");
let hist_hash = FnvHasher::hash_bytes(b"latency");
let mut seq = 1u64;
for _ in 0..5 {
let payload = encode_metric_payload(&MetricEvent::counter(counter_hash, 1.0));
consumer
.queue
.enqueue(Message::new(test_sender(), seq, payload.to_vec()))
.unwrap();
seq += 1;
}
for v in [50.0, 60.0, 70.0] {
let payload = encode_metric_payload(&MetricEvent::gauge(gauge_hash, v));
consumer
.queue
.enqueue(Message::new(test_sender(), seq, payload.to_vec()))
.unwrap();
seq += 1;
}
for v in [10.0, 20.0, 30.0, 100.0] {
let payload = encode_metric_payload(&MetricEvent::histogram(hist_hash, v));
consumer
.queue
.enqueue(Message::new(test_sender(), seq, payload.to_vec()))
.unwrap();
seq += 1;
}
let processed = consumer.drain();
assert_eq!(processed, 12);
let slot = consumer.pipeline.get_slot(counter_hash).unwrap();
assert_eq!(slot.counter, 5.0);
let slot = consumer.pipeline.get_slot(gauge_hash).unwrap();
assert_eq!(slot.gauge, 70.0);
let slot = consumer.pipeline.get_slot(hist_hash).unwrap();
assert_eq!(slot.ddsketch.count(), 4);
}
#[test]
fn test_drain_batch_limit() {
let mut consumer = QueueConsumerPipeline::<64, 256, 128>::new(0.05);
let hash = FnvHasher::hash_bytes(b"metric");
for i in 0..20u64 {
let payload = encode_metric_payload(&MetricEvent::counter(hash, 1.0));
consumer
.queue
.enqueue(Message::new(test_sender(), i + 1, payload.to_vec()))
.unwrap();
}
let processed = consumer.drain_batch(5);
assert_eq!(processed, 5);
assert_eq!(consumer.queue.len(), 15);
let processed = consumer.drain();
assert_eq!(processed, 15);
assert!(consumer.queue.is_empty());
let slot = consumer.pipeline.get_slot(hash).unwrap();
assert_eq!(slot.counter, 20.0);
}
}