use std::cell::UnsafeCell;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::sync::Notify;
use crate::data::metrics::MetricType;
use crate::metrics::ContextKey;
use crate::Tag;
const RING_SIZE: usize = 2048;
const RING_MASK: u64 = RING_SIZE as u64 - 1;
const NOTIFY_INTERVAL: u64 = 1024;
const NOTIFY_MASK: u64 = NOTIFY_INTERVAL - 1;
const READY_BIT: u64 = 1 << 63;
struct Slot {
value: UnsafeCell<f64>,
tags: UnsafeCell<Vec<Tag>>,
ready: AtomicU64,
}
impl Slot {
fn empty() -> Self {
Slot {
value: UnsafeCell::new(0.0),
tags: UnsafeCell::new(Vec::new()),
ready: AtomicU64::new(0),
}
}
}
fn metric_type_to_bits(t: MetricType) -> u64 {
match t {
MetricType::Gauge => 0,
MetricType::Count => 1,
MetricType::Distribution => 2,
MetricType::Rate => 3,
}
}
fn metric_type_from_bits(b: u64) -> MetricType {
match b & 0b11 {
0 => MetricType::Gauge,
1 => MetricType::Count,
2 => MetricType::Distribution,
_ => MetricType::Rate,
}
}
fn encode_key(key: ContextKey) -> u64 {
READY_BIT | (metric_type_to_bits(key.metric_type()) << 32) | key.index() as u64
}
fn decode_key(v: u64) -> ContextKey {
ContextKey::from_parts((v & 0xFFFF_FFFF) as u32, metric_type_from_bits(v >> 32))
}
pub struct MetricRing {
slots: Box<[Slot]>,
write_pos: AtomicU64,
read_pos: AtomicU64,
notify: Notify,
}
unsafe impl Send for MetricRing {}
unsafe impl Sync for MetricRing {}
impl MetricRing {
pub fn new() -> Self {
let slots = (0..RING_SIZE).map(|_| Slot::empty()).collect::<Vec<_>>();
MetricRing {
slots: slots.into_boxed_slice(),
write_pos: AtomicU64::new(0),
read_pos: AtomicU64::new(0),
notify: Notify::new(),
}
}
pub fn notified(&self) -> impl std::future::Future<Output = ()> + '_ {
self.notify.notified()
}
pub fn push(&self, value: f64, key: ContextKey, tags: Vec<Tag>) {
let seq = self.write_pos.fetch_add(1, Ordering::Relaxed);
let mut spins = 0u32;
while seq.wrapping_sub(self.read_pos.load(Ordering::Acquire)) >= RING_SIZE as u64 {
self.notify.notify_one();
spins += 1;
if spins < 64 {
std::hint::spin_loop();
} else {
std::thread::yield_now();
}
}
let slot = &self.slots[(seq & RING_MASK) as usize];
unsafe {
*slot.value.get() = value;
*slot.tags.get() = tags;
}
slot.ready.store(encode_key(key), Ordering::Release);
if seq & NOTIFY_MASK == NOTIFY_MASK {
self.notify.notify_one();
}
}
pub fn drain(&self, mut f: impl FnMut(f64, ContextKey, Vec<Tag>)) {
loop {
let seq = self.read_pos.load(Ordering::Relaxed);
let slot = &self.slots[(seq & RING_MASK) as usize];
let encoded = slot.ready.load(Ordering::Acquire);
if encoded == 0 {
break;
}
let value = unsafe { *slot.value.get() };
let tags = unsafe { std::mem::take(&mut *slot.tags.get()) };
let key = decode_key(encoded);
slot.ready.store(0, Ordering::Release);
self.read_pos.store(seq.wrapping_add(1), Ordering::Release);
f(value, key, tags);
}
}
}
impl Default for MetricRing {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
fn key(index: u32, t: MetricType) -> ContextKey {
ContextKey::from_parts(index, t)
}
#[test]
fn encode_decode_roundtrip() {
for (idx, t) in [
(0u32, MetricType::Gauge),
(1, MetricType::Count),
(u32::MAX, MetricType::Distribution),
(12345, MetricType::Rate),
] {
let k = key(idx, t);
let e = encode_key(k);
assert_ne!(
e, 0,
"encoded key must never collide with the empty sentinel"
);
assert_eq!(decode_key(e), k);
}
}
#[test]
fn single_producer_single_consumer_preserves_all_points() {
let ring = MetricRing::new();
let n = RING_SIZE as u32 * 4; let mut consumed = 0u64;
let mut sum = 0.0f64;
let mut next_expected = 0u32;
for i in 0..n {
ring.push(i as f64, key(i, MetricType::Count), Vec::new());
ring.drain(|v, k, _| {
assert_eq!(k.index(), next_expected, "points must arrive in order");
assert_eq!(v as u32, next_expected);
next_expected += 1;
consumed += 1;
sum += v;
});
}
assert_eq!(consumed, n as u64);
assert_eq!(sum, (0..n).map(|i| i as f64).sum::<f64>());
}
#[cfg_attr(miri, ignore)] #[test]
fn multi_producer_batch_drain_loses_nothing() {
let ring = Arc::new(MetricRing::new());
let producers = 4u32;
let per_producer = 50_000u32;
let done = Arc::new(AtomicBool::new(false));
let consumed = Arc::new(AtomicU64::new(0));
let value_sum = Arc::new(AtomicU64::new(0));
let consumer = {
let ring = ring.clone();
let done = done.clone();
let consumed = consumed.clone();
let value_sum = value_sum.clone();
std::thread::spawn(move || loop {
ring.drain(|v, _k, _t| {
consumed.fetch_add(1, Ordering::Relaxed);
value_sum.fetch_add(v as u64, Ordering::Relaxed);
});
if done.load(Ordering::Acquire)
&& ring.read_pos.load(Ordering::Acquire)
== ring.write_pos.load(Ordering::Acquire)
{
break;
}
std::hint::spin_loop();
})
};
let mut handles = Vec::new();
for _ in 0..producers {
let ring = ring.clone();
handles.push(std::thread::spawn(move || {
for i in 0..per_producer {
ring.push(i as f64, key(i, MetricType::Count), Vec::new());
}
}));
}
for h in handles {
h.join().unwrap();
}
done.store(true, Ordering::Release);
consumer.join().unwrap();
let total = (producers * per_producer) as u64;
assert_eq!(
consumed.load(Ordering::Relaxed),
total,
"no points lost or duplicated"
);
let expected_sum = producers as u64 * (0..per_producer as u64).sum::<u64>();
assert_eq!(value_sum.load(Ordering::Relaxed), expected_sum);
}
}