use super::{GaugeValue, Registered, raw};
use std::num::NonZeroU32;
mod sealed {
pub trait Sealed: Clone {
type Value: Copy + Default + PartialEq;
fn change(from: Self::Value, to: Self::Value) -> Self::Value;
fn apply(&self, change: Self::Value);
}
}
pub trait Batchable: sealed::Sealed {}
impl Batchable for raw::Counter {}
impl Batchable for raw::Gauge {}
impl sealed::Sealed for raw::Counter {
type Value = u64;
fn change(from: u64, to: u64) -> u64 {
to.wrapping_sub(from)
}
fn apply(&self, change: u64) {
self.inc_by(change);
}
}
impl sealed::Sealed for raw::Gauge {
type Value = GaugeValue;
fn change(from: GaugeValue, to: GaugeValue) -> GaugeValue {
to.wrapping_sub(from)
}
fn apply(&self, change: GaugeValue) {
self.inc_by(change);
}
}
pub struct Batched<M: Batchable> {
shared: M,
value: M::Value,
published: M::Value,
pending: u32,
size: NonZeroU32,
}
impl<M: Batchable> Batched<M> {
pub fn new(shared: &Registered<M>, size: NonZeroU32) -> Self {
Self {
shared: shared.metric().clone(),
value: M::Value::default(),
published: M::Value::default(),
pending: 0,
size,
}
}
pub const fn get(&self) -> M::Value {
self.value
}
fn publish(&mut self) {
if self.value != self.published {
self.shared.apply(M::change(self.published, self.value));
self.published = self.value;
}
self.pending = 0;
}
fn update(&mut self, value: M::Value) {
self.value = value;
self.pending += 1;
if self.pending == self.size.get() {
self.publish();
}
}
}
impl<M: Batchable> Drop for Batched<M> {
fn drop(&mut self) {
self.publish();
}
}
impl Batched<raw::Counter> {
pub fn inc(&mut self) {
self.update(self.value.wrapping_add(1));
}
pub fn inc_by(&mut self, value: u64) {
self.update(self.value.wrapping_add(value));
}
}
impl Batched<raw::Gauge> {
pub fn inc(&mut self) {
self.update(self.value.wrapping_add(1));
}
pub fn dec(&mut self) {
self.update(self.value.wrapping_sub(1));
}
pub fn inc_by(&mut self, value: GaugeValue) {
self.update(self.value.wrapping_add(value));
}
pub fn dec_by(&mut self, value: GaugeValue) {
self.update(self.value.wrapping_sub(value));
}
pub fn set(&mut self, value: GaugeValue) {
self.update(value);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
Metrics as _, Runner as _, deterministic,
telemetry::metrics::{MetricsExt as _, has_metric_value, metric_samples},
};
use commonware_utils::NZU32;
#[test]
fn test_counter_publishes_full_batches() {
let shared = Registered::detached(raw::Counter::default());
let mut batched = Batched::new(&shared, NZU32!(3));
batched.inc();
batched.inc();
assert_eq!(shared.get(), 0);
assert_eq!(batched.get(), 2);
batched.inc();
assert_eq!(shared.get(), 3);
batched.inc();
batched.inc();
assert_eq!(shared.get(), 3);
batched.inc();
assert_eq!(shared.get(), 6);
batched.inc_by(5);
assert_eq!(shared.get(), 6);
assert_eq!(batched.get(), 11);
drop(batched);
assert_eq!(shared.get(), 11);
}
#[test]
fn test_gauge_publishes_net_change() {
let shared = Registered::detached(raw::Gauge::default());
let mut batched = Batched::new(&shared, NZU32!(2));
batched.inc();
batched.dec();
assert_eq!(shared.get(), 0);
batched.dec_by(3);
batched.inc();
assert_eq!(shared.get(), -2);
batched.set(5);
batched.set(5);
assert_eq!(shared.get(), 5);
}
#[test]
fn test_handles_publish_independently() {
let shared = Registered::detached(raw::Gauge::default());
let mut first = Batched::new(&shared, NZU32!(1));
first.inc_by(3);
assert_eq!(shared.get(), 3);
let mut second = Batched::new(&shared, NZU32!(1));
second.inc_by(4);
assert_eq!(second.get(), 4);
assert_eq!(shared.get(), 7);
let mut third = Batched::new(&shared, NZU32!(2));
third.inc();
assert_eq!(shared.get(), 7);
drop(third);
assert_eq!(shared.get(), 8);
}
#[test]
fn test_registration_stays_with_owner() {
deterministic::Runner::default().start(|context| async move {
let shared = context.gauge("keys", "Number of keys");
let mut batched = Batched::new(&shared, NZU32!(1));
batched.inc_by(5);
assert!(has_metric_value(&context.encode(), "keys", 5));
drop(shared);
assert!(metric_samples(&context.encode(), "keys").next().is_none());
let fresh = context.gauge("keys", "Number of keys");
batched.inc();
assert_eq!(fresh.get(), 0);
});
}
}