use crate::window_cell::WindowCell;
use crate::{
Counter, CounterGroup, Gauge, GaugeGroup, Lazy, LazyCounter, LazyGauge, Metric, Value,
};
use metriken_core::Window;
use std::collections::HashMap;
pub struct WindowedLazyCounter {
inner: LazyCounter,
window: WindowCell,
}
impl WindowedLazyCounter {
pub const fn new(f: fn() -> Counter) -> Self {
Self {
inner: LazyCounter::new(f),
window: WindowCell::new(),
}
}
pub fn set_with_window(&self, value: u64, window: Window) {
self.window.with_write(|w| {
self.inner.set(value);
*w = Some(window);
});
}
pub fn load_with_window(&self) -> (Option<u64>, Option<Window>) {
self.window
.with_read(|w| (Lazy::get(&self.inner).map(|c| c.value()), w))
}
}
impl Metric for WindowedLazyCounter {
fn is_enabled(&self) -> bool {
self.inner.is_enabled()
}
fn as_any(&self) -> Option<&dyn std::any::Any> {
self.inner.as_any()
}
fn value(&self) -> Option<Value<'_>> {
self.inner.value()
}
fn load_window(&self) -> Option<Window> {
self.window.load()
}
fn value_with_window(&self) -> (Option<Value<'_>>, Option<Window>) {
self.window.with_read(|w| (self.inner.value(), w))
}
}
pub struct WindowedLazyGauge {
inner: LazyGauge,
window: WindowCell,
}
impl WindowedLazyGauge {
pub const fn new(f: fn() -> Gauge) -> Self {
Self {
inner: LazyGauge::new(f),
window: WindowCell::new(),
}
}
pub fn set_with_window(&self, value: i64, window: Window) {
self.window.with_write(|w| {
self.inner.set(value);
*w = Some(window);
});
}
pub fn load_with_window(&self) -> (Option<i64>, Option<Window>) {
self.window
.with_read(|w| (Lazy::get(&self.inner).map(|g| g.value()), w))
}
}
impl Metric for WindowedLazyGauge {
fn is_enabled(&self) -> bool {
self.inner.is_enabled()
}
fn as_any(&self) -> Option<&dyn std::any::Any> {
self.inner.as_any()
}
fn value(&self) -> Option<Value<'_>> {
self.inner.value()
}
fn load_window(&self) -> Option<Window> {
self.window.load()
}
fn value_with_window(&self) -> (Option<Value<'_>>, Option<Window>) {
self.window.with_read(|w| (self.inner.value(), w))
}
}
pub struct WindowedCounterGroup {
inner: CounterGroup,
}
impl WindowedCounterGroup {
pub const fn new(entries: usize) -> Self {
Self {
inner: CounterGroup::new(entries),
}
}
pub fn entries(&self) -> usize {
self.inner.entries()
}
pub fn set_with_window(&self, idx: usize, value: u64, window: Window) -> bool {
self.inner.set_with_window(idx, value, window)
}
pub fn load_with_window(&self, idx: usize) -> (Option<u64>, Option<Window>) {
self.inner.load_with_window(idx)
}
pub fn value(&self, idx: usize) -> Option<u64> {
self.inner.value(idx)
}
pub fn set_metadata(&self, idx: usize, metadata: HashMap<String, String>) {
self.inner.set_metadata(idx, metadata)
}
pub fn insert_metadata(&self, idx: usize, key: String, value: String) {
self.inner.insert_metadata(idx, key, value)
}
pub fn load_metadata(&self, idx: usize) -> Option<HashMap<String, String>> {
self.inner.load_metadata(idx)
}
pub fn metadata_snapshot(&self) -> Vec<(usize, HashMap<String, String>)> {
self.inner.metadata_snapshot()
}
pub fn clear_metadata(&self, idx: usize) {
self.inner.clear_metadata(idx)
}
}
impl Metric for WindowedCounterGroup {
fn as_any(&self) -> Option<&dyn std::any::Any> {
Some(self)
}
fn value(&self) -> Option<Value<'_>> {
Some(Value::CounterGroup(&self.inner))
}
}
pub struct WindowedGaugeGroup {
inner: GaugeGroup,
}
impl WindowedGaugeGroup {
pub const fn new(entries: usize) -> Self {
Self {
inner: GaugeGroup::new(entries),
}
}
pub fn entries(&self) -> usize {
self.inner.entries()
}
pub fn set_with_window(&self, idx: usize, value: i64, window: Window) -> bool {
self.inner.set_with_window(idx, value, window)
}
pub fn load_with_window(&self, idx: usize) -> (Option<i64>, Option<Window>) {
self.inner.load_with_window(idx)
}
pub fn value(&self, idx: usize) -> Option<i64> {
self.inner.value(idx)
}
pub fn set_metadata(&self, idx: usize, metadata: HashMap<String, String>) {
self.inner.set_metadata(idx, metadata)
}
pub fn insert_metadata(&self, idx: usize, key: String, value: String) {
self.inner.insert_metadata(idx, key, value)
}
pub fn load_metadata(&self, idx: usize) -> Option<HashMap<String, String>> {
self.inner.load_metadata(idx)
}
pub fn metadata_snapshot(&self) -> Vec<(usize, HashMap<String, String>)> {
self.inner.metadata_snapshot()
}
pub fn clear_metadata(&self, idx: usize) {
self.inner.clear_metadata(idx)
}
}
impl Metric for WindowedGaugeGroup {
fn as_any(&self) -> Option<&dyn std::any::Any> {
Some(self)
}
fn value(&self) -> Option<Value<'_>> {
Some(Value::GaugeGroup(&self.inner))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Counter, Gauge, Metric, Value};
use metriken_core::Window;
use std::sync::Arc;
use std::thread;
#[test]
fn counter_group_round_trip() {
use metriken_core::Window;
let g = WindowedCounterGroup::new(4);
assert!(g.set_with_window(1, 55, Window::new(10, 20)));
assert_eq!(g.load_with_window(1), (Some(55), Some(Window::new(10, 20))));
assert_eq!(g.value(1), Some(55));
assert_eq!(g.entries(), 4);
assert!(!g.set_with_window(9, 1, Window::new(1, 2)));
assert_eq!(g.load_with_window(9), (None, None));
if let Some(Value::CounterGroup(inner)) = <WindowedCounterGroup as Metric>::value(&g) {
assert_eq!(
inner.load_with_window(1),
(Some(55), Some(Window::new(10, 20)))
);
} else {
panic!("expected Value::CounterGroup");
}
}
#[test]
fn counter_group_metadata_round_trips() {
let g = WindowedCounterGroup::new(2);
g.insert_metadata(0, "cpu".into(), "0".into());
assert_eq!(g.load_metadata(0).unwrap().get("cpu").unwrap(), "0");
assert_eq!(g.metadata_snapshot().len(), 1);
}
#[test]
fn counter_group_torn_read_stress() {
use metriken_core::Window;
use std::sync::Arc;
use std::thread;
const ITERS: u64 = 200_000;
let g = Arc::new(WindowedCounterGroup::new(1));
g.set_with_window(0, 0, Window::new(0, 1));
let writer = {
let g = g.clone();
thread::spawn(move || {
for v in 1..ITERS {
g.set_with_window(0, v, Window::new(v, v + 1));
}
})
};
let reader = {
let g = g.clone();
thread::spawn(move || {
for _ in 0..ITERS {
let (v, w) = g.load_with_window(0);
if let (Some(v), Some(w)) = (v, w) {
assert_eq!(w.begin_ns, v, "torn read: value {v} paired with {w:?}");
assert_eq!(w.end_ns, v + 1, "torn read: value {v} paired with {w:?}");
}
}
})
};
writer.join().unwrap();
reader.join().unwrap();
}
#[test]
fn round_trip() {
let c = WindowedLazyCounter::new(Counter::new);
c.set_with_window(42, Window::new(10, 20));
assert_eq!(c.load_with_window(), (Some(42), Some(Window::new(10, 20))));
assert!(matches!(
<WindowedLazyCounter as Metric>::value(&c),
Some(Value::Counter(42))
));
assert_eq!(
<WindowedLazyCounter as Metric>::load_window(&c),
Some(Window::new(10, 20))
);
}
#[test]
fn unset_window_is_none_and_value_uninitialized() {
let c = WindowedLazyCounter::new(Counter::new);
assert_eq!(c.load_with_window(), (None, None));
assert!(<WindowedLazyCounter as Metric>::load_window(&c).is_none());
}
#[test]
fn value_with_window_pairs_atomically() {
let c = WindowedLazyCounter::new(Counter::new);
c.set_with_window(42, Window::new(10, 20));
let (value, window) = <WindowedLazyCounter as Metric>::value_with_window(&c);
assert!(matches!(value, Some(Value::Counter(42))));
assert_eq!(window, Some(Window::new(10, 20)));
}
#[test]
fn base_primitives_unchanged_and_window_cell_pointer_sized() {
use crate::window_cell::WindowCell;
use parking_lot::RwLock;
use std::sync::atomic::{AtomicI64, AtomicU64};
use std::sync::OnceLock;
assert_eq!(
std::mem::size_of::<Counter>(),
std::mem::size_of::<AtomicU64>(),
"Counter must stay exactly its AtomicU64 (option A)"
);
assert_eq!(
std::mem::size_of::<Gauge>(),
std::mem::size_of::<AtomicI64>(),
"Gauge must stay exactly its AtomicI64 (option A)"
);
assert_eq!(
std::mem::size_of::<WindowCell>(),
std::mem::size_of::<OnceLock<Box<RwLock<Option<Window>>>>>()
);
let c = WindowedLazyCounter::new(Counter::new);
assert!(<WindowedLazyCounter as Metric>::load_window(&c).is_none());
}
#[test]
fn torn_read_stress() {
const ITERS: u64 = 200_000;
let c = Arc::new(WindowedLazyCounter::new(Counter::new));
c.set_with_window(0, Window::new(0, 1));
let writer = {
let c = c.clone();
thread::spawn(move || {
for v in 1..ITERS {
c.set_with_window(v, Window::new(v, v + 1));
}
})
};
let reader = {
let c = c.clone();
thread::spawn(move || {
for _ in 0..ITERS {
let (v, w) = c.load_with_window();
if let (Some(v), Some(w)) = (v, w) {
assert_eq!(w.begin_ns, v, "torn read: value {v} paired with {w:?}");
assert_eq!(w.end_ns, v + 1, "torn read: value {v} paired with {w:?}");
}
}
})
};
writer.join().unwrap();
reader.join().unwrap();
}
#[test]
fn gauge_round_trip() {
let g = WindowedLazyGauge::new(Gauge::new);
g.set_with_window(-7, Window::new(100, 250));
assert_eq!(
g.load_with_window(),
(Some(-7), Some(Window::new(100, 250)))
);
assert_eq!(
<WindowedLazyGauge as Metric>::load_window(&g),
Some(Window::new(100, 250))
);
}
#[test]
fn gauge_unset_window_is_none() {
let g = WindowedLazyGauge::new(Gauge::new);
assert_eq!(g.load_with_window(), (None, None));
assert!(<WindowedLazyGauge as Metric>::load_window(&g).is_none());
}
#[test]
fn gauge_value_with_window_pairs_atomically() {
let g = WindowedLazyGauge::new(Gauge::new);
g.set_with_window(-7, Window::new(100, 250));
let (value, window) = <WindowedLazyGauge as Metric>::value_with_window(&g);
assert!(matches!(value, Some(Value::Gauge(-7))));
assert_eq!(window, Some(Window::new(100, 250)));
}
#[test]
fn gauge_group_round_trip() {
use metriken_core::Window;
let g = WindowedGaugeGroup::new(4);
assert!(g.set_with_window(1, -12, Window::new(10, 20)));
assert_eq!(
g.load_with_window(1),
(Some(-12), Some(Window::new(10, 20)))
);
assert_eq!(g.value(1), Some(-12));
assert_eq!(g.entries(), 4);
assert!(!g.set_with_window(9, 1, Window::new(1, 2)));
assert_eq!(g.load_with_window(9), (None, None));
if let Some(Value::GaugeGroup(inner)) = <WindowedGaugeGroup as Metric>::value(&g) {
assert_eq!(
inner.load_with_window(1),
(Some(-12), Some(Window::new(10, 20)))
);
} else {
panic!("expected Value::GaugeGroup");
}
}
#[test]
fn gauge_group_metadata_round_trips() {
let g = WindowedGaugeGroup::new(2);
g.insert_metadata(0, "cpu".into(), "0".into());
assert_eq!(g.load_metadata(0).unwrap().get("cpu").unwrap(), "0");
assert_eq!(g.metadata_snapshot().len(), 1);
}
}