commonware_runtime/telemetry/metrics/
batched.rs1use super::{GaugeValue, Registered, raw};
4use std::num::NonZeroU32;
5
6mod sealed {
7 pub trait Sealed: Clone {
9 type Value: Copy + Default + PartialEq;
11
12 fn change(from: Self::Value, to: Self::Value) -> Self::Value;
14
15 fn apply(&self, change: Self::Value);
17 }
18}
19
20pub trait Batchable: sealed::Sealed {}
23
24impl Batchable for raw::Counter {}
25
26impl Batchable for raw::Gauge {}
27
28impl sealed::Sealed for raw::Counter {
29 type Value = u64;
30
31 fn change(from: u64, to: u64) -> u64 {
32 to.wrapping_sub(from)
33 }
34
35 fn apply(&self, change: u64) {
36 self.inc_by(change);
37 }
38}
39
40impl sealed::Sealed for raw::Gauge {
41 type Value = GaugeValue;
42
43 fn change(from: GaugeValue, to: GaugeValue) -> GaugeValue {
44 to.wrapping_sub(from)
45 }
46
47 fn apply(&self, change: GaugeValue) {
48 self.inc_by(change);
49 }
50}
51
52pub struct Batched<M: Batchable> {
62 shared: M,
64
65 value: M::Value,
67
68 published: M::Value,
71
72 pending: u32,
74
75 size: NonZeroU32,
77}
78
79impl<M: Batchable> Batched<M> {
80 pub fn new(shared: &Registered<M>, size: NonZeroU32) -> Self {
88 Self {
89 shared: shared.metric().clone(),
90 value: M::Value::default(),
91 published: M::Value::default(),
92 pending: 0,
93 size,
94 }
95 }
96
97 pub const fn get(&self) -> M::Value {
99 self.value
100 }
101
102 fn publish(&mut self) {
105 if self.value != self.published {
106 self.shared.apply(M::change(self.published, self.value));
107 self.published = self.value;
108 }
109 self.pending = 0;
110 }
111
112 fn update(&mut self, value: M::Value) {
114 self.value = value;
115 self.pending += 1;
116 if self.pending == self.size.get() {
117 self.publish();
118 }
119 }
120}
121
122impl<M: Batchable> Drop for Batched<M> {
123 fn drop(&mut self) {
124 self.publish();
125 }
126}
127
128impl Batched<raw::Counter> {
129 pub fn inc(&mut self) {
131 self.update(self.value.wrapping_add(1));
132 }
133
134 pub fn inc_by(&mut self, value: u64) {
136 self.update(self.value.wrapping_add(value));
137 }
138}
139
140impl Batched<raw::Gauge> {
141 pub fn inc(&mut self) {
143 self.update(self.value.wrapping_add(1));
144 }
145
146 pub fn dec(&mut self) {
148 self.update(self.value.wrapping_sub(1));
149 }
150
151 pub fn inc_by(&mut self, value: GaugeValue) {
153 self.update(self.value.wrapping_add(value));
154 }
155
156 pub fn dec_by(&mut self, value: GaugeValue) {
158 self.update(self.value.wrapping_sub(value));
159 }
160
161 pub fn set(&mut self, value: GaugeValue) {
163 self.update(value);
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::{
171 Metrics as _, Runner as _, deterministic,
172 telemetry::metrics::{MetricsExt as _, has_metric_value, metric_samples},
173 };
174 use commonware_utils::NZU32;
175
176 #[test]
177 fn test_counter_publishes_full_batches() {
178 let shared = Registered::detached(raw::Counter::default());
180 let mut batched = Batched::new(&shared, NZU32!(3));
181 batched.inc();
182 batched.inc();
183 assert_eq!(shared.get(), 0);
184 assert_eq!(batched.get(), 2);
185 batched.inc();
186 assert_eq!(shared.get(), 3);
187 batched.inc();
188 batched.inc();
189 assert_eq!(shared.get(), 3);
190 batched.inc();
191 assert_eq!(shared.get(), 6);
192
193 batched.inc_by(5);
195 assert_eq!(shared.get(), 6);
196 assert_eq!(batched.get(), 11);
197 drop(batched);
198 assert_eq!(shared.get(), 11);
199 }
200
201 #[test]
202 fn test_gauge_publishes_net_change() {
203 let shared = Registered::detached(raw::Gauge::default());
205 let mut batched = Batched::new(&shared, NZU32!(2));
206 batched.inc();
207 batched.dec();
208 assert_eq!(shared.get(), 0);
209
210 batched.dec_by(3);
212 batched.inc();
213 assert_eq!(shared.get(), -2);
214
215 batched.set(5);
218 batched.set(5);
219 assert_eq!(shared.get(), 5);
220 }
221
222 #[test]
223 fn test_handles_publish_independently() {
224 let shared = Registered::detached(raw::Gauge::default());
226 let mut first = Batched::new(&shared, NZU32!(1));
227 first.inc_by(3);
228 assert_eq!(shared.get(), 3);
229
230 let mut second = Batched::new(&shared, NZU32!(1));
232 second.inc_by(4);
233 assert_eq!(second.get(), 4);
234 assert_eq!(shared.get(), 7);
235
236 let mut third = Batched::new(&shared, NZU32!(2));
238 third.inc();
239 assert_eq!(shared.get(), 7);
240 drop(third);
241 assert_eq!(shared.get(), 8);
242 }
243
244 #[test]
245 fn test_registration_stays_with_owner() {
246 deterministic::Runner::default().start(|context| async move {
247 let shared = context.gauge("keys", "Number of keys");
249 let mut batched = Batched::new(&shared, NZU32!(1));
250 batched.inc_by(5);
251 assert!(has_metric_value(&context.encode(), "keys", 5));
252
253 drop(shared);
257 assert!(metric_samples(&context.encode(), "keys").next().is_none());
258 let fresh = context.gauge("keys", "Number of keys");
259 batched.inc();
260 assert_eq!(fresh.get(), 0);
261 });
262 }
263}