fast_telemetry/metric/dynamic/
gauge_i64.rs1use super::cache::{CacheableSeries, LabelCache, SERIES_CACHE_SIZE};
8#[cfg(feature = "eviction")]
9use super::current_cycle;
10use super::{DynamicIndexMap, DynamicLabelSet, dynamic_index_map, thread_id};
11use crossbeam_utils::CachePadded;
12use parking_lot::RwLock;
13use std::cell::RefCell;
14#[cfg(feature = "eviction")]
15use std::sync::atomic::AtomicU32;
16use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, AtomicUsize, Ordering};
17use std::sync::{Arc, Weak};
18
19static GAUGE_I64_IDS: AtomicUsize = AtomicUsize::new(1);
20const DEFAULT_MAX_SERIES: usize = 2000;
21const OVERFLOW_LABEL_KEY: &str = "__ft_overflow";
22const OVERFLOW_LABEL_VALUE: &str = "true";
23type GaugeI64IndexShard = CachePadded<RwLock<DynamicIndexMap<Arc<GaugeI64Series>>>>;
24
25struct GaugeI64Series {
26 cells: Vec<CachePadded<AtomicI64>>,
27 evicted: AtomicBool,
29 #[cfg(feature = "eviction")]
31 last_accessed_cycle: AtomicU32,
32}
33
34impl GaugeI64Series {
35 #[cfg(feature = "eviction")]
36 fn new(shard_count: usize, cycle: u32) -> Self {
37 Self {
38 cells: (0..shard_count)
39 .map(|_| CachePadded::new(AtomicI64::new(0)))
40 .collect(),
41 evicted: AtomicBool::new(false),
42 last_accessed_cycle: AtomicU32::new(cycle),
43 }
44 }
45
46 #[cfg(not(feature = "eviction"))]
47 fn new(shard_count: usize) -> Self {
48 Self {
49 cells: (0..shard_count)
50 .map(|_| CachePadded::new(AtomicI64::new(0)))
51 .collect(),
52 evicted: AtomicBool::new(false),
53 }
54 }
55
56 #[inline]
57 fn add_at(&self, shard_idx: usize, value: i64) {
58 self.cells[shard_idx].fetch_add(value, Ordering::Relaxed);
59 }
62
63 #[inline]
64 fn set_at(&self, shard_idx: usize, value: i64) {
65 for (i, cell) in self.cells.iter().enumerate() {
68 if i == shard_idx {
69 cell.store(value, Ordering::Relaxed);
70 } else {
71 cell.store(0, Ordering::Relaxed);
72 }
73 }
74 }
77
78 #[cfg(feature = "eviction")]
80 #[inline]
81 fn touch(&self, cycle: u32) {
82 self.last_accessed_cycle.store(cycle, Ordering::Relaxed);
83 }
84
85 #[inline]
86 fn sum(&self) -> i64 {
87 self.cells
88 .iter()
89 .map(|cell| cell.load(Ordering::Relaxed))
90 .sum()
91 }
92
93 #[inline]
94 fn is_evicted(&self) -> bool {
95 self.evicted.load(Ordering::Relaxed)
96 }
97
98 #[cfg(feature = "eviction")]
99 fn mark_evicted(&self) {
100 self.evicted.store(true, Ordering::Relaxed);
101 }
102}
103
104impl CacheableSeries for GaugeI64Series {
105 fn is_evicted(&self) -> bool {
106 self.is_evicted()
107 }
108}
109
110#[derive(Clone)]
116pub struct DynamicGaugeI64Series {
117 series: Arc<GaugeI64Series>,
118 shard_mask: usize,
119}
120
121impl DynamicGaugeI64Series {
122 #[inline]
124 pub fn inc(&self) {
125 self.add(1);
126 }
127
128 #[inline]
130 pub fn dec(&self) {
131 self.add(-1);
132 }
133
134 #[inline]
136 pub fn add(&self, value: i64) {
137 let shard_idx = thread_id() & self.shard_mask;
138 self.series.add_at(shard_idx, value);
139 }
140
141 #[inline]
143 pub fn set(&self, value: i64) {
144 let shard_idx = thread_id() & self.shard_mask;
145 self.series.set_at(shard_idx, value);
146 }
147
148 #[inline]
150 pub fn get(&self) -> i64 {
151 self.series.sum()
152 }
153
154 #[inline]
156 pub fn is_evicted(&self) -> bool {
157 self.series.is_evicted()
158 }
159}
160
161thread_local! {
162 static SERIES_CACHE: RefCell<LabelCache<Weak<GaugeI64Series>, SERIES_CACHE_SIZE>> =
163 RefCell::new(LabelCache::new());
164}
165
166pub struct DynamicGaugeI64 {
173 id: usize,
174 shard_count: usize,
175 max_series: usize,
176 shard_mask: usize,
177 index_shards: Vec<GaugeI64IndexShard>,
178 series_count: AtomicUsize,
180 overflow_count: AtomicU64,
182}
183
184impl DynamicGaugeI64 {
185 pub fn new(shard_count: usize) -> Self {
187 Self::with_max_series(shard_count, DEFAULT_MAX_SERIES)
188 }
189
190 pub fn with_max_series(shard_count: usize, max_series: usize) -> Self {
198 let shard_count = shard_count.next_power_of_two();
199 let id = GAUGE_I64_IDS.fetch_add(1, Ordering::Relaxed);
200 Self {
201 id,
202 shard_count,
203 max_series,
204 shard_mask: shard_count - 1,
205 index_shards: (0..shard_count)
206 .map(|_| CachePadded::new(RwLock::new(dynamic_index_map())))
207 .collect(),
208 series_count: AtomicUsize::new(0),
209 overflow_count: AtomicU64::new(0),
210 }
211 }
212
213 pub fn series(&self, labels: &[(&str, &str)]) -> DynamicGaugeI64Series {
215 if let Some(series) = self.cached_series(labels) {
216 return DynamicGaugeI64Series {
217 series,
218 shard_mask: self.shard_mask,
219 };
220 }
221 let series = self.lookup_or_create(labels);
222 self.update_cache(labels, &series);
223 DynamicGaugeI64Series {
224 series,
225 shard_mask: self.shard_mask,
226 }
227 }
228
229 #[inline]
231 pub fn inc(&self, labels: &[(&str, &str)]) {
232 self.add(labels, 1);
233 }
234
235 #[inline]
237 pub fn dec(&self, labels: &[(&str, &str)]) {
238 self.add(labels, -1);
239 }
240
241 #[inline]
243 pub fn add(&self, labels: &[(&str, &str)], value: i64) {
244 if let Some(series) = self.cached_series(labels) {
245 let shard_idx = thread_id() & self.shard_mask;
246 series.add_at(shard_idx, value);
247 return;
248 }
249
250 let series = self.lookup_or_create(labels);
251 self.update_cache(labels, &series);
252 let shard_idx = thread_id() & self.shard_mask;
253 series.add_at(shard_idx, value);
254 }
255
256 #[inline]
258 pub fn set(&self, labels: &[(&str, &str)], value: i64) {
259 if let Some(series) = self.cached_series(labels) {
260 let shard_idx = thread_id() & self.shard_mask;
261 series.set_at(shard_idx, value);
262 return;
263 }
264
265 let series = self.lookup_or_create(labels);
266 self.update_cache(labels, &series);
267 let shard_idx = thread_id() & self.shard_mask;
268 series.set_at(shard_idx, value);
269 }
270
271 pub fn get(&self, labels: &[(&str, &str)]) -> i64 {
273 let key = DynamicLabelSet::from_pairs(labels);
274 let index_shard = self.index_shard_for(&key);
275 self.index_shards[index_shard]
276 .read()
277 .get(&key)
278 .map(|series| series.sum())
279 .unwrap_or(0)
280 }
281
282 pub fn sum_all(&self) -> i64 {
284 self.index_shards
285 .iter()
286 .map(|shard| {
287 let guard = shard.read();
288 guard.values().map(|series| series.sum()).sum::<i64>()
289 })
290 .sum()
291 }
292
293 pub fn snapshot(&self) -> Vec<(DynamicLabelSet, i64)> {
295 let mut out = Vec::new();
296 for shard in &self.index_shards {
297 let guard = shard.read();
298 for (labels, series) in guard.iter() {
299 out.push((labels.clone(), series.sum()));
300 }
301 }
302 out
303 }
304
305 pub fn cardinality(&self) -> usize {
307 self.index_shards
308 .iter()
309 .map(|shard| shard.read().len())
310 .sum()
311 }
312
313 pub fn overflow_count(&self) -> u64 {
318 self.overflow_count.load(Ordering::Relaxed)
319 }
320
321 #[doc(hidden)]
328 pub fn visit_series(&self, mut f: impl FnMut(&[(String, String)], i64)) {
329 for shard in &self.index_shards {
330 let guard = shard.read();
331 for (labels, series) in guard.iter() {
332 f(labels.pairs(), series.sum());
333 }
334 }
335 }
336
337 #[cfg(feature = "eviction")]
348 pub fn evict_stale(&self, max_staleness: u32) -> usize {
349 let cycle = current_cycle();
350 let mut removed = 0;
351
352 for shard in &self.index_shards {
353 let mut guard = shard.write();
354 guard.retain(|_labels, series| {
355 if Arc::strong_count(series) > 1 {
358 return true;
359 }
360 let last = series.last_accessed_cycle.load(Ordering::Relaxed);
362 let stale = cycle.saturating_sub(last) > max_staleness;
363 if stale {
364 series.mark_evicted();
365 removed += 1;
366 self.series_count.fetch_sub(1, Ordering::Relaxed);
367 }
368 !stale
369 });
370 }
371
372 removed
373 }
374
375 fn lookup_or_create(&self, labels: &[(&str, &str)]) -> Arc<GaugeI64Series> {
376 let requested_key = DynamicLabelSet::from_pairs(labels);
377 let requested_shard = self.index_shard_for(&requested_key);
378 #[cfg(feature = "eviction")]
379 let cycle = current_cycle();
380
381 if let Some(series) = self.index_shards[requested_shard]
383 .read()
384 .get(&requested_key)
385 {
386 #[cfg(feature = "eviction")]
387 series.touch(cycle);
388 return Arc::clone(series);
389 }
390
391 let key = if self.max_series > 0
393 && self.series_count.load(Ordering::Relaxed) >= self.max_series
394 {
395 self.overflow_count.fetch_add(1, Ordering::Relaxed);
396 DynamicLabelSet::from_pairs(&[(OVERFLOW_LABEL_KEY, OVERFLOW_LABEL_VALUE)])
397 } else {
398 requested_key
399 };
400 let shard = self.index_shard_for(&key);
401
402 if let Some(series) = self.index_shards[shard].read().get(&key) {
403 #[cfg(feature = "eviction")]
404 series.touch(cycle);
405 return Arc::clone(series);
406 }
407
408 let mut guard = self.index_shards[shard].write();
409 if let Some(series) = guard.get(&key) {
410 #[cfg(feature = "eviction")]
411 series.touch(cycle);
412 return Arc::clone(series);
413 }
414 #[cfg(feature = "eviction")]
415 let series = Arc::new(GaugeI64Series::new(self.shard_count, cycle));
416 #[cfg(not(feature = "eviction"))]
417 let series = Arc::new(GaugeI64Series::new(self.shard_count));
418 guard.insert(key, Arc::clone(&series));
419 self.series_count.fetch_add(1, Ordering::Relaxed);
420 series
421 }
422
423 fn index_shard_for(&self, key: &DynamicLabelSet) -> usize {
424 key.shard_index(self.shard_mask)
425 }
426
427 fn cached_series(&self, labels: &[(&str, &str)]) -> Option<Arc<GaugeI64Series>> {
428 SERIES_CACHE.with(|cache| {
429 let series = cache.borrow_mut().get(self.id, labels)?;
430 #[cfg(feature = "eviction")]
431 series.touch(current_cycle());
432 Some(series)
433 })
434 }
435
436 fn update_cache(&self, labels: &[(&str, &str)], series: &Arc<GaugeI64Series>) {
437 SERIES_CACHE.with(|cache| {
438 cache
439 .borrow_mut()
440 .insert(self.id, labels, Arc::downgrade(series));
441 });
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 #[cfg(feature = "eviction")]
448 use super::super::{advance_cycle, lock_eviction_cycle_for_test};
449 use super::*;
450
451 #[test]
452 fn test_basic_operations() {
453 let gauge = DynamicGaugeI64::new(4);
454 gauge.inc(&[("endpoint_id", "ep1")]);
455 gauge.add(&[("endpoint_id", "ep1")], 2);
456
457 assert_eq!(gauge.get(&[("endpoint_id", "ep1")]), 3);
458
459 gauge.dec(&[("endpoint_id", "ep1")]);
460 assert_eq!(gauge.get(&[("endpoint_id", "ep1")]), 2);
461
462 gauge.add(&[("endpoint_id", "ep1")], -2);
463 assert_eq!(gauge.get(&[("endpoint_id", "ep1")]), 0);
464 }
465
466 #[test]
467 fn test_series_handle() {
468 let gauge = DynamicGaugeI64::new(4);
469 let series = gauge.series(&[("endpoint_id", "ep1")]);
470 series.inc();
471 series.inc();
472 series.dec();
473
474 assert_eq!(series.get(), 1);
475 assert_eq!(gauge.get(&[("endpoint_id", "ep1")]), 1);
476 }
477
478 #[test]
479 fn test_snapshot() {
480 let gauge = DynamicGaugeI64::new(4);
481 gauge.add(&[("endpoint_id", "ep1")], 10);
482 gauge.add(&[("endpoint_id", "ep2")], 20);
483
484 let snap = gauge.snapshot();
485 assert_eq!(snap.len(), 2);
486
487 let total: i64 = snap.iter().map(|(_, v)| v).sum();
488 assert_eq!(total, 30);
489 }
490
491 #[cfg(feature = "eviction")]
492 #[test]
493 fn test_evict_stale() {
494 let _cycle_guard = lock_eviction_cycle_for_test();
495 let gauge = DynamicGaugeI64::new(4);
496 let labels = &[("endpoint_id", "evict_i64")];
497
498 gauge.add(labels, 5);
499 assert_eq!(gauge.cardinality(), 1);
500
501 advance_cycle();
503 advance_cycle();
504
505 gauge.add(&[("flush", "cache")], 1);
507
508 let removed = gauge.evict_stale(1);
509 assert_eq!(removed, 1);
510 assert_eq!(gauge.cardinality(), 1); assert_eq!(gauge.get(labels), 0);
512 }
513
514 #[cfg(feature = "eviction")]
515 #[test]
516 fn test_series_handle_protects_from_eviction() {
517 let _cycle_guard = lock_eviction_cycle_for_test();
518 let gauge = DynamicGaugeI64::new(4);
519 let labels = &[("endpoint_id", "tombstone_i64")];
520
521 let series = gauge.series(labels);
522 series.add(5);
523 assert!(!series.is_evicted());
524
525 advance_cycle();
527 advance_cycle();
528 let removed = gauge.evict_stale(1);
529
530 assert_eq!(removed, 0);
532 assert!(!series.is_evicted());
533 assert_eq!(gauge.get(labels), 5);
534 }
535
536 #[test]
537 fn test_overflow_bucket_routes_new_series_at_capacity() {
538 let gauge = DynamicGaugeI64::with_max_series(4, 1);
539 gauge.add(&[("endpoint_id", "1")], 1);
540 gauge.add(&[("endpoint_id", "2")], 2);
541
542 assert_eq!(gauge.cardinality(), 2);
543 assert_eq!(gauge.get(&[(OVERFLOW_LABEL_KEY, OVERFLOW_LABEL_VALUE)]), 2);
544 }
545}