1use super::cache::{CacheableSeries, LabelCache, SERIES_CACHE_SIZE};
4#[cfg(feature = "eviction")]
5use super::current_cycle;
6use super::{COUNTER_IDS, DynamicIndexMap, DynamicLabelSet, dynamic_index_map, thread_id};
7use crossbeam_utils::CachePadded;
8use parking_lot::RwLock;
9use std::cell::RefCell;
10#[cfg(feature = "eviction")]
11use std::sync::atomic::AtomicU32;
12use std::sync::atomic::{AtomicBool, AtomicIsize, AtomicU64, AtomicUsize, Ordering};
13use std::sync::{Arc, Weak};
14
15const DEFAULT_MAX_SERIES: usize = 2000;
16const OVERFLOW_LABEL_KEY: &str = "__ft_overflow";
17const OVERFLOW_LABEL_VALUE: &str = "true";
18
19struct CounterSeries {
20 cells: Vec<CachePadded<AtomicIsize>>,
21 evicted: AtomicBool,
24 #[cfg(feature = "eviction")]
27 last_accessed_cycle: AtomicU32,
28}
29
30type CounterIndexShard = CachePadded<RwLock<DynamicIndexMap<Arc<CounterSeries>>>>;
31
32impl CounterSeries {
33 #[cfg(feature = "eviction")]
34 fn new(shard_count: usize, current_cycle: u32) -> Self {
35 Self {
36 cells: (0..shard_count)
37 .map(|_| CachePadded::new(AtomicIsize::new(0)))
38 .collect(),
39 evicted: AtomicBool::new(false),
40 last_accessed_cycle: AtomicU32::new(current_cycle),
41 }
42 }
43
44 #[cfg(not(feature = "eviction"))]
45 fn new(shard_count: usize) -> Self {
46 Self {
47 cells: (0..shard_count)
48 .map(|_| CachePadded::new(AtomicIsize::new(0)))
49 .collect(),
50 evicted: AtomicBool::new(false),
51 }
52 }
53
54 #[inline]
55 fn add_at(&self, shard_idx: usize, value: isize) {
56 self.cells[shard_idx].fetch_add(value, Ordering::Relaxed);
57 }
60
61 #[cfg(feature = "eviction")]
63 #[inline]
64 fn touch(&self, cycle: u32) {
65 self.last_accessed_cycle.store(cycle, Ordering::Relaxed);
66 }
67
68 #[inline]
69 fn sum(&self) -> isize {
70 self.cells
71 .iter()
72 .map(|cell| cell.load(Ordering::Relaxed))
73 .sum()
74 }
75
76 #[inline]
77 fn is_evicted(&self) -> bool {
78 self.evicted.load(Ordering::Relaxed)
79 }
80
81 #[cfg(feature = "eviction")]
82 fn mark_evicted(&self) {
83 self.evicted.store(true, Ordering::Relaxed);
84 }
85}
86
87impl CacheableSeries for CounterSeries {
88 fn is_evicted(&self) -> bool {
89 self.is_evicted()
90 }
91}
92
93#[derive(Clone)]
99pub struct DynamicCounterSeries {
100 series: Arc<CounterSeries>,
101 shard_mask: usize,
102}
103
104impl DynamicCounterSeries {
105 #[inline]
107 pub fn inc(&self) {
108 self.add(1);
109 }
110
111 #[inline]
113 pub fn add(&self, value: isize) {
114 let shard_idx = thread_id() & self.shard_mask;
115 self.series.add_at(shard_idx, value);
116 }
117
118 #[inline]
120 pub fn get(&self) -> isize {
121 self.series.sum()
122 }
123
124 #[inline]
130 pub fn is_evicted(&self) -> bool {
131 self.series.is_evicted()
132 }
133}
134
135thread_local! {
136 static SERIES_CACHE: RefCell<LabelCache<Weak<CounterSeries>, SERIES_CACHE_SIZE>> =
137 RefCell::new(LabelCache::new());
138}
139
140pub struct DynamicCounter {
145 id: usize,
146 shard_count: usize,
147 max_series: usize,
148 shard_mask: usize,
149 index_shards: Vec<CounterIndexShard>,
150 series_count: AtomicUsize,
152 overflow_count: AtomicU64,
154}
155
156impl DynamicCounter {
157 pub fn new(shard_count: usize) -> Self {
159 Self::with_max_series(shard_count, DEFAULT_MAX_SERIES)
160 }
161
162 pub fn with_max_series(shard_count: usize, max_series: usize) -> Self {
170 let shard_count = shard_count.next_power_of_two();
171 let id = COUNTER_IDS.fetch_add(1, Ordering::Relaxed);
172 Self {
173 id,
174 shard_count,
175 max_series,
176 shard_mask: shard_count - 1,
177 index_shards: (0..shard_count)
178 .map(|_| CachePadded::new(RwLock::new(dynamic_index_map())))
179 .collect(),
180 series_count: AtomicUsize::new(0),
181 overflow_count: AtomicU64::new(0),
182 }
183 }
184
185 pub fn series(&self, labels: &[(&str, &str)]) -> DynamicCounterSeries {
189 if let Some(series) = self.cached_series(labels) {
190 return DynamicCounterSeries {
191 series,
192 shard_mask: self.shard_mask,
193 };
194 }
195 let series = self.lookup_or_create(labels);
196 self.update_cache(labels, &series);
197 DynamicCounterSeries {
198 series,
199 shard_mask: self.shard_mask,
200 }
201 }
202
203 #[inline]
205 pub fn inc(&self, labels: &[(&str, &str)]) {
206 self.add(labels, 1);
207 }
208
209 #[inline]
211 pub fn add(&self, labels: &[(&str, &str)], value: isize) {
212 if let Some(series) = self.cached_series(labels) {
213 let shard_idx = thread_id() & self.shard_mask;
214 series.add_at(shard_idx, value);
215 return;
216 }
217
218 let series = self.lookup_or_create(labels);
219 self.update_cache(labels, &series);
220 let shard_idx = thread_id() & self.shard_mask;
221 series.add_at(shard_idx, value);
222 }
223
224 pub fn get(&self, labels: &[(&str, &str)]) -> isize {
226 let key = DynamicLabelSet::from_pairs(labels);
227 let index_shard = self.index_shard_for(&key);
228 self.index_shards[index_shard]
229 .read()
230 .get(&key)
231 .map(|series| series.sum())
232 .unwrap_or(0)
233 }
234
235 pub fn sum_all(&self) -> isize {
237 self.index_shards
238 .iter()
239 .map(|shard| {
240 let guard = shard.read();
241 guard.values().map(|series| series.sum()).sum::<isize>()
242 })
243 .sum()
244 }
245
246 pub fn snapshot(&self) -> Vec<(DynamicLabelSet, isize)> {
248 let mut out = Vec::new();
249 for shard in &self.index_shards {
250 let guard = shard.read();
251 for (labels, series) in guard.iter() {
252 out.push((labels.clone(), series.sum()));
253 }
254 }
255 out
256 }
257
258 pub fn cardinality(&self) -> usize {
260 self.index_shards
261 .iter()
262 .map(|shard| shard.read().len())
263 .sum()
264 }
265
266 pub fn overflow_count(&self) -> u64 {
271 self.overflow_count.load(Ordering::Relaxed)
272 }
273
274 #[doc(hidden)]
281 pub fn visit_series(&self, mut f: impl FnMut(&[(String, String)], isize)) {
282 for shard in &self.index_shards {
283 let guard = shard.read();
284 for (labels, series) in guard.iter() {
285 f(labels.pairs(), series.sum());
286 }
287 }
288 }
289
290 #[cfg(feature = "eviction")]
301 pub fn evict_stale(&self, max_staleness: u32) -> usize {
302 let cycle = current_cycle();
303 let mut removed = 0;
304
305 for shard in &self.index_shards {
306 let mut guard = shard.write();
307 guard.retain(|_labels, series| {
308 if Arc::strong_count(series) > 1 {
311 return true;
312 }
313 let last = series.last_accessed_cycle.load(Ordering::Relaxed);
315 let stale = cycle.saturating_sub(last) > max_staleness;
316 if stale {
317 series.mark_evicted();
318 removed += 1;
319 self.series_count.fetch_sub(1, Ordering::Relaxed);
320 }
321 !stale
322 });
323 }
324
325 removed
326 }
327
328 fn lookup_or_create(&self, labels: &[(&str, &str)]) -> Arc<CounterSeries> {
329 let requested_key = DynamicLabelSet::from_pairs(labels);
330 let requested_shard = self.index_shard_for(&requested_key);
331 #[cfg(feature = "eviction")]
332 let cycle = current_cycle();
333
334 if let Some(series) = self.index_shards[requested_shard]
336 .read()
337 .get(&requested_key)
338 {
339 #[cfg(feature = "eviction")]
340 series.touch(cycle);
341 return Arc::clone(series);
342 }
343
344 let key = if self.max_series > 0
349 && self.series_count.load(Ordering::Relaxed) >= self.max_series
350 {
351 self.overflow_count.fetch_add(1, Ordering::Relaxed);
352 DynamicLabelSet::from_pairs(&[(OVERFLOW_LABEL_KEY, OVERFLOW_LABEL_VALUE)])
353 } else {
354 requested_key
355 };
356 let shard = self.index_shard_for(&key);
357
358 if let Some(series) = self.index_shards[shard].read().get(&key) {
360 #[cfg(feature = "eviction")]
361 series.touch(cycle);
362 return Arc::clone(series);
363 }
364
365 let mut guard = self.index_shards[shard].write();
366 if let Some(series) = guard.get(&key) {
367 #[cfg(feature = "eviction")]
368 series.touch(cycle);
369 return Arc::clone(series);
370 }
371 #[cfg(feature = "eviction")]
372 let series = Arc::new(CounterSeries::new(self.shard_count, cycle));
373 #[cfg(not(feature = "eviction"))]
374 let series = Arc::new(CounterSeries::new(self.shard_count));
375 guard.insert(key, Arc::clone(&series));
376 self.series_count.fetch_add(1, Ordering::Relaxed);
377 series
378 }
379
380 fn index_shard_for(&self, key: &DynamicLabelSet) -> usize {
381 key.shard_index(self.shard_mask)
382 }
383
384 fn cached_series(&self, labels: &[(&str, &str)]) -> Option<Arc<CounterSeries>> {
385 SERIES_CACHE.with(|cache| {
386 let series = cache.borrow_mut().get(self.id, labels)?;
387 #[cfg(feature = "eviction")]
388 series.touch(current_cycle());
389 Some(series)
390 })
391 }
392
393 fn update_cache(&self, labels: &[(&str, &str)], series: &Arc<CounterSeries>) {
394 SERIES_CACHE.with(|cache| {
395 cache
396 .borrow_mut()
397 .insert(self.id, labels, Arc::downgrade(series));
398 });
399 }
400}
401
402#[cfg(test)]
403mod tests {
404 #[cfg(feature = "eviction")]
405 use super::super::{advance_cycle, lock_eviction_cycle_for_test};
406 use super::*;
407
408 #[test]
409 fn test_basic_operations() {
410 let counter = DynamicCounter::new(4);
411 counter.inc(&[("org_id", "42"), ("endpoint_uuid", "abc")]);
412 counter.add(&[("org_id", "42"), ("endpoint_uuid", "abc")], 2);
413
414 assert_eq!(
415 counter.get(&[("org_id", "42"), ("endpoint_uuid", "abc")]),
416 3
417 );
418 assert_eq!(counter.sum_all(), 3);
419 }
420
421 #[test]
422 fn test_label_order_is_canonicalized() {
423 let counter = DynamicCounter::new(4);
424 counter.inc(&[("org_id", "42"), ("endpoint_uuid", "abc")]);
425
426 assert_eq!(
427 counter.get(&[("endpoint_uuid", "abc"), ("org_id", "42")]),
428 1
429 );
430 }
431
432 #[test]
433 fn test_series_handle() {
434 let counter = DynamicCounter::new(4);
435 let series = counter.series(&[("org_id", "42"), ("endpoint_uuid", "abc")]);
436 series.inc();
437 series.add(9);
438
439 assert_eq!(series.get(), 10);
440 assert_eq!(
441 counter.get(&[("org_id", "42"), ("endpoint_uuid", "abc")]),
442 10
443 );
444 }
445
446 #[test]
447 fn test_concurrent_adds() {
448 let counter = DynamicCounter::new(8);
449 let series = counter.series(&[("org_id", "42"), ("endpoint_uuid", "abc")]);
450
451 std::thread::scope(|s| {
452 for _ in 0..8 {
453 let series = series.clone();
454 s.spawn(move || {
455 for _ in 0..10_000 {
456 series.inc();
457 }
458 });
459 }
460 });
461
462 assert_eq!(
463 counter.get(&[("org_id", "42"), ("endpoint_uuid", "abc")]),
464 80_000
465 );
466 }
467
468 #[cfg(feature = "eviction")]
469 #[test]
470 fn test_evict_stale() {
471 let _cycle_guard = lock_eviction_cycle_for_test();
472 let counter = DynamicCounter::new(4);
473 let labels = &[("org_id", "42")];
474
475 counter.inc(labels);
477 assert_eq!(counter.cardinality(), 1);
478 assert_eq!(counter.get(labels), 1);
479
480 advance_cycle();
482 advance_cycle();
483
484 counter.inc(&[("flush", "cache")]);
486
487 let removed = counter.evict_stale(1);
489 assert_eq!(removed, 1); assert_eq!(counter.cardinality(), 1); assert_eq!(counter.get(labels), 0);
494
495 counter.inc(labels);
497 assert_eq!(counter.cardinality(), 2);
498 assert_eq!(counter.get(labels), 1);
499 }
500
501 #[cfg(feature = "eviction")]
502 #[test]
503 fn test_evict_stale_keeps_active() {
504 let _cycle_guard = lock_eviction_cycle_for_test();
505 let counter = DynamicCounter::new(4);
506 let active = &[("status", "active")];
507 let stale = &[("status", "stale")];
508
509 counter.inc(active);
511 counter.inc(stale);
512 assert_eq!(counter.cardinality(), 2);
513
514 advance_cycle();
516
517 counter.inc(active);
519
520 advance_cycle();
522
523 let removed = counter.evict_stale(1);
525 assert_eq!(removed, 1);
526 assert_eq!(counter.cardinality(), 1);
527 assert_eq!(counter.get(active), 2);
528 assert_eq!(counter.get(stale), 0);
529 }
530
531 #[cfg(feature = "eviction")]
532 #[test]
533 fn test_eviction_tombstone_invalidates_cache() {
534 let _cycle_guard = lock_eviction_cycle_for_test();
535 let counter = DynamicCounter::new(4);
536 let labels = &[("org_id", "evict_test")];
537
538 counter.inc(labels);
540 counter.inc(labels); assert_eq!(counter.get(labels), 2);
542
543 advance_cycle();
545 advance_cycle();
546
547 counter.inc(&[("flush", "cache")]);
549
550 counter.evict_stale(1);
551
552 counter.inc(labels);
554 assert_eq!(counter.get(labels), 1); }
556
557 #[cfg(feature = "eviction")]
558 #[test]
559 fn test_series_handle_protects_from_eviction() {
560 let _cycle_guard = lock_eviction_cycle_for_test();
561 let counter = DynamicCounter::new(4);
562 let labels = &[("org_id", "handle_test")];
563
564 let series = counter.series(labels);
566 series.inc();
567 assert!(!series.is_evicted());
568
569 advance_cycle();
571 advance_cycle();
572 let removed = counter.evict_stale(1);
573
574 assert_eq!(removed, 0);
576 assert!(!series.is_evicted());
577 assert_eq!(counter.cardinality(), 1);
578 assert_eq!(counter.get(labels), 1);
579
580 series.inc();
582 assert_eq!(counter.get(labels), 2);
583 }
584
585 #[cfg(feature = "eviction")]
586 #[test]
587 fn test_series_evicted_after_handle_dropped() {
588 let _cycle_guard = lock_eviction_cycle_for_test();
589 let counter = DynamicCounter::new(4);
590 let labels = &[("org_id", "handle_drop_test")];
591
592 {
594 let series = counter.series(labels);
595 series.inc();
596 }
597 assert_eq!(counter.cardinality(), 1);
600 assert_eq!(counter.get(labels), 1);
601
602 advance_cycle();
604 advance_cycle();
605
606 counter.inc(&[("flush", "cache")]);
608
609 let removed = counter.evict_stale(1);
611 assert_eq!(removed, 1);
612 assert_eq!(counter.get(labels), 0);
613 }
614
615 #[test]
616 fn test_overflow_bucket_routes_new_series_at_capacity() {
617 let counter = DynamicCounter::with_max_series(4, 2);
618
619 counter.inc(&[("org_id", "1")]);
620 counter.inc(&[("org_id", "2")]);
621 counter.inc(&[("org_id", "3")]);
622
623 assert_eq!(counter.cardinality(), 3);
624 assert_eq!(
625 counter.get(&[(OVERFLOW_LABEL_KEY, OVERFLOW_LABEL_VALUE)]),
626 1
627 );
628 }
629
630 #[test]
631 fn test_concurrent_cap_bounded_overshoot() {
632 use std::sync::{Arc, Barrier};
633 use std::thread;
634
635 let cap = 10;
636 let threads = 16;
637 let counter = Arc::new(DynamicCounter::with_max_series(4, cap));
638 let barrier = Arc::new(Barrier::new(threads));
639
640 let handles: Vec<_> = (0..threads)
641 .map(|t| {
642 let counter = Arc::clone(&counter);
643 let barrier = Arc::clone(&barrier);
644 thread::spawn(move || {
645 barrier.wait();
646 for i in 0..5 {
648 let label = format!("t{t}_s{i}");
649 counter.inc(&[("key", &label)]);
650 }
651 })
652 })
653 .collect();
654
655 for h in handles {
656 h.join().unwrap();
657 }
658
659 let card = counter.cardinality();
660 assert!(
663 card <= cap + threads + 1, "cardinality {card} exceeded bounded overshoot (cap={cap}, threads={threads})"
665 );
666 assert!(
668 counter.overflow_count() > 0,
669 "overflow should have triggered"
670 );
671 }
672
673 #[cfg(feature = "eviction")]
674 #[test]
675 fn test_eviction_and_reinsertion_bookkeeping() {
676 let _cycle_guard = lock_eviction_cycle_for_test();
677 let counter = DynamicCounter::with_max_series(4, 3);
678
679 counter.inc(&[("k", "a")]);
680 counter.inc(&[("k", "b")]);
681 counter.inc(&[("k", "c")]);
682 assert_eq!(counter.cardinality(), 3);
683
684 counter.inc(&[("k", "d")]);
685 assert!(counter.overflow_count() > 0);
686 let card_after_overflow = counter.cardinality();
687 assert!(card_after_overflow <= 4);
688
689 advance_cycle();
690 advance_cycle();
691 advance_cycle();
692 counter.inc(&[("flush", "cache")]);
693 let evicted = counter.evict_stale(1);
694 assert!(evicted > 0);
695
696 let card_after_evict = counter.cardinality();
697 assert!(
698 card_after_evict < card_after_overflow,
699 "cardinality should decrease after eviction: before={card_after_overflow} after={card_after_evict}"
700 );
701
702 let overflow_before = counter.overflow_count();
703 counter.inc(&[("k", "new1")]);
704 counter.inc(&[("k", "new2")]);
705
706 assert!(counter.cardinality() <= 5);
707
708 let overflow_after = counter.overflow_count();
709 assert!(
710 overflow_after - overflow_before <= 1,
711 "unexpected overflow after eviction freed space"
712 );
713 }
714}