1use super::cache::{CacheValue, LabelCache, SERIES_CACHE_SIZE};
8#[cfg(feature = "eviction")]
9use super::current_cycle;
10use super::{DISTRIBUTION_IDS, DynamicIndexMap, DynamicLabelSet, dynamic_index_map};
11use crate::exp_buckets::{ExpBuckets, ExpBucketsSnapshot};
12use crossbeam_utils::CachePadded;
13use parking_lot::{Mutex, RwLock};
14use std::cell::RefCell;
15use std::sync::Arc;
16use std::sync::Weak;
17#[cfg(feature = "eviction")]
18use std::sync::atomic::AtomicU32;
19use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
20
21const DEFAULT_MAX_SERIES: usize = 2000;
22const OVERFLOW_LABEL_KEY: &str = "__ft_overflow";
23const OVERFLOW_LABEL_VALUE: &str = "true";
24type DistributionIndexShard = CachePadded<RwLock<DynamicIndexMap<Arc<DistributionSeries>>>>;
25type DistributionSnapshotEntry = (DynamicLabelSet, u64, u64, ExpBucketsSnapshot);
26static SERIES_IDS: AtomicUsize = AtomicUsize::new(1);
27
28struct DistributionSeries {
29 id: usize,
30 registry: Mutex<Vec<Arc<ExpBuckets>>>,
31 evicted: AtomicBool,
33 #[cfg(feature = "eviction")]
35 last_accessed_cycle: AtomicU32,
36}
37
38impl DistributionSeries {
39 #[cfg(feature = "eviction")]
40 fn new(cycle: u32) -> Self {
41 Self {
42 id: SERIES_IDS.fetch_add(1, Ordering::Relaxed),
43 registry: Mutex::new(Vec::new()),
44 evicted: AtomicBool::new(false),
45 last_accessed_cycle: AtomicU32::new(cycle),
46 }
47 }
48
49 #[cfg(not(feature = "eviction"))]
50 fn new() -> Self {
51 Self {
52 id: SERIES_IDS.fetch_add(1, Ordering::Relaxed),
53 registry: Mutex::new(Vec::new()),
54 evicted: AtomicBool::new(false),
55 }
56 }
57
58 #[cfg(feature = "eviction")]
60 #[inline]
61 fn touch(&self, cycle: u32) {
62 self.last_accessed_cycle.store(cycle, Ordering::Relaxed);
63 }
64
65 #[inline]
66 fn is_evicted(&self) -> bool {
67 self.evicted.load(Ordering::Relaxed)
68 }
69
70 #[cfg(feature = "eviction")]
71 fn mark_evicted(&self) {
72 self.evicted.store(true, Ordering::Relaxed);
73 }
74
75 fn get_or_create_buf(&self) -> Arc<ExpBuckets> {
76 let buf = Arc::new(ExpBuckets::new());
77 self.registry.lock().push(Arc::clone(&buf));
78 buf
79 }
80
81 fn count(&self) -> u64 {
82 self.registry.lock().iter().map(|buf| buf.get_count()).sum()
83 }
84
85 fn sum(&self) -> u64 {
86 self.registry.lock().iter().map(|buf| buf.get_sum()).sum()
87 }
88
89 fn buckets_snapshot(&self) -> ExpBucketsSnapshot {
90 let mut positive = [0u64; 64];
91 let mut zero_count = 0u64;
92 let mut sum = 0u64;
93 let mut count = 0u64;
94
95 let registry = self.registry.lock();
96 for buf in registry.iter() {
97 let thread_buckets = buf.get_positive_buckets();
98 for (i, &c) in thread_buckets.iter().enumerate() {
99 positive[i] += c;
100 }
101 zero_count += buf.get_zero_count();
102 sum += buf.get_sum();
103 count += buf.get_count();
104 }
105
106 ExpBucketsSnapshot {
107 positive,
108 zero_count,
109 sum,
110 count,
111 }
112 }
113}
114
115struct DistributionCacheValue {
116 series: Weak<DistributionSeries>,
117 buf: Arc<ExpBuckets>,
118}
119
120impl CacheValue for DistributionCacheValue {
121 type Strong = (Arc<DistributionSeries>, Arc<ExpBuckets>);
122
123 fn upgrade(&self) -> Option<Self::Strong> {
124 Some((self.series.upgrade()?, Arc::clone(&self.buf)))
125 }
126
127 fn is_valid(strong: &Self::Strong) -> bool {
128 !strong.0.is_evicted()
129 }
130}
131
132#[derive(Clone)]
138pub struct DynamicDistributionSeries {
139 series: Arc<DistributionSeries>,
140 buf: Arc<ExpBuckets>,
141}
142
143impl DynamicDistributionSeries {
144 #[inline]
146 pub fn record(&self, value: u64) {
147 self.buf.record(value);
148 }
149
150 pub fn count(&self) -> u64 {
152 self.series.count()
153 }
154
155 pub fn sum(&self) -> u64 {
157 self.series.sum()
158 }
159
160 #[inline]
162 pub fn is_evicted(&self) -> bool {
163 self.series.is_evicted()
164 }
165}
166
167thread_local! {
168 static SERIES_CACHE: RefCell<LabelCache<DistributionCacheValue, SERIES_CACHE_SIZE>> =
169 RefCell::new(LabelCache::new());
170 static SERIES_BUF_CACHE: RefCell<Vec<(usize, usize, Weak<ExpBuckets>)>> = const { RefCell::new(Vec::new()) };
171}
172
173pub struct DynamicDistribution {
177 id: usize,
178 max_series: usize,
179 shard_mask: usize,
180 index_shards: Vec<DistributionIndexShard>,
181 series_count: AtomicUsize,
183 overflow_count: AtomicU64,
185}
186
187impl DynamicDistribution {
188 pub fn new(shard_count: usize) -> Self {
190 Self::with_max_series(shard_count, DEFAULT_MAX_SERIES)
191 }
192
193 pub fn with_max_series(shard_count: usize, max_series: usize) -> Self {
195 let shard_count = shard_count.next_power_of_two();
196 let id = DISTRIBUTION_IDS.fetch_add(1, Ordering::Relaxed);
197 Self {
198 id,
199 max_series,
200 shard_mask: shard_count - 1,
201 index_shards: (0..shard_count)
202 .map(|_| CachePadded::new(RwLock::new(dynamic_index_map())))
203 .collect(),
204 series_count: AtomicUsize::new(0),
205 overflow_count: AtomicU64::new(0),
206 }
207 }
208
209 pub fn series(&self, labels: &[(&str, &str)]) -> DynamicDistributionSeries {
213 if let Some((series, buf)) = self.cached_series(labels) {
214 return DynamicDistributionSeries { series, buf };
215 }
216 let series = self.lookup_or_create(labels);
217 let buf = self.get_or_create_thread_buf(&series);
218 self.update_cache(labels, &series, Arc::clone(&buf));
219 DynamicDistributionSeries { series, buf }
220 }
221
222 #[inline]
224 pub fn record(&self, labels: &[(&str, &str)], value: u64) {
225 if let Some((_series, buf)) = self.cached_series(labels) {
226 buf.record(value);
227 return;
228 }
229
230 let series = self.lookup_or_create(labels);
231 let buf = self.get_or_create_thread_buf(&series);
232 self.update_cache(labels, &series, Arc::clone(&buf));
233 buf.record(value);
234 }
235
236 pub fn count(&self, labels: &[(&str, &str)]) -> u64 {
238 let key = DynamicLabelSet::from_pairs(labels);
239 let index_shard = self.index_shard_for(&key);
240 self.index_shards[index_shard]
241 .read()
242 .get(&key)
243 .map(|series| series.count())
244 .unwrap_or(0)
245 }
246
247 pub fn sum(&self, labels: &[(&str, &str)]) -> u64 {
249 let key = DynamicLabelSet::from_pairs(labels);
250 let index_shard = self.index_shard_for(&key);
251 self.index_shards[index_shard]
252 .read()
253 .get(&key)
254 .map(|series| series.sum())
255 .unwrap_or(0)
256 }
257
258 pub fn snapshot(&self) -> Vec<DistributionSnapshotEntry> {
260 let mut out = Vec::new();
261 for shard in &self.index_shards {
262 let guard = shard.read();
263 for (labels, series) in guard.iter() {
264 let snap = series.buckets_snapshot();
265 out.push((labels.clone(), snap.count, snap.sum, snap));
266 }
267 }
268 out
269 }
270
271 pub fn cardinality(&self) -> usize {
273 self.index_shards
274 .iter()
275 .map(|shard| shard.read().len())
276 .sum()
277 }
278
279 pub fn overflow_count(&self) -> u64 {
284 self.overflow_count.load(Ordering::Relaxed)
285 }
286
287 #[doc(hidden)]
294 pub fn visit_series(
295 &self,
296 mut f: impl FnMut(&[(String, String)], u64, u64, ExpBucketsSnapshot),
297 ) {
298 for shard in &self.index_shards {
299 let guard = shard.read();
300 for (labels, series) in guard.iter() {
301 let snap = series.buckets_snapshot();
302 f(labels.pairs(), snap.count, snap.sum, snap);
303 }
304 }
305 }
306
307 #[cfg(feature = "eviction")]
318 pub fn evict_stale(&self, max_staleness: u32) -> usize {
319 let cycle = current_cycle();
320 let mut removed = 0;
321
322 for shard in &self.index_shards {
323 let mut guard = shard.write();
324 guard.retain(|_labels, series| {
325 if Arc::strong_count(series) > 1 {
328 return true;
329 }
330 let last = series.last_accessed_cycle.load(Ordering::Relaxed);
332 let stale = cycle.saturating_sub(last) > max_staleness;
333 if stale {
334 series.mark_evicted();
335 removed += 1;
336 self.series_count.fetch_sub(1, Ordering::Relaxed);
337 }
338 !stale
339 });
340 }
341
342 removed
343 }
344
345 fn lookup_or_create(&self, labels: &[(&str, &str)]) -> Arc<DistributionSeries> {
346 let requested_key = DynamicLabelSet::from_pairs(labels);
347 let requested_shard = self.index_shard_for(&requested_key);
348 #[cfg(feature = "eviction")]
349 let cycle = current_cycle();
350
351 if let Some(series) = self.index_shards[requested_shard]
353 .read()
354 .get(&requested_key)
355 {
356 #[cfg(feature = "eviction")]
357 series.touch(cycle);
358 return Arc::clone(series);
359 }
360
361 let key = if self.max_series > 0
363 && self.series_count.load(Ordering::Relaxed) >= self.max_series
364 {
365 self.overflow_count.fetch_add(1, Ordering::Relaxed);
366 DynamicLabelSet::from_pairs(&[(OVERFLOW_LABEL_KEY, OVERFLOW_LABEL_VALUE)])
367 } else {
368 requested_key
369 };
370 let shard = self.index_shard_for(&key);
371
372 if let Some(series) = self.index_shards[shard].read().get(&key) {
373 #[cfg(feature = "eviction")]
374 series.touch(cycle);
375 return Arc::clone(series);
376 }
377
378 let mut guard = self.index_shards[shard].write();
379 if let Some(series) = guard.get(&key) {
380 #[cfg(feature = "eviction")]
381 series.touch(cycle);
382 return Arc::clone(series);
383 }
384 #[cfg(feature = "eviction")]
385 let series = Arc::new(DistributionSeries::new(cycle));
386 #[cfg(not(feature = "eviction"))]
387 let series = Arc::new(DistributionSeries::new());
388 guard.insert(key, Arc::clone(&series));
389 self.series_count.fetch_add(1, Ordering::Relaxed);
390 series
391 }
392
393 fn index_shard_for(&self, key: &DynamicLabelSet) -> usize {
394 key.shard_index(self.shard_mask)
395 }
396
397 fn cached_series(
398 &self,
399 labels: &[(&str, &str)],
400 ) -> Option<(Arc<DistributionSeries>, Arc<ExpBuckets>)> {
401 SERIES_CACHE.with(|cache| {
402 let (series, buf) = cache.borrow_mut().get(self.id, labels)?;
403 #[cfg(feature = "eviction")]
404 series.touch(current_cycle());
405 Some((series, buf))
406 })
407 }
408
409 fn update_cache(
410 &self,
411 labels: &[(&str, &str)],
412 series: &Arc<DistributionSeries>,
413 buf: Arc<ExpBuckets>,
414 ) {
415 SERIES_CACHE.with(|cache| {
416 cache.borrow_mut().insert(
417 self.id,
418 labels,
419 DistributionCacheValue {
420 series: Arc::downgrade(series),
421 buf,
422 },
423 );
424 });
425 }
426
427 fn get_or_create_thread_buf(&self, series: &Arc<DistributionSeries>) -> Arc<ExpBuckets> {
428 let dist_id = self.id;
429 let series_id = series.id;
430
431 SERIES_BUF_CACHE.with(|cache| {
432 let mut entries = cache.borrow_mut();
433 entries.retain(|(_id, _ptr, weak)| weak.strong_count() > 0);
434
435 for (id, ptr, weak) in entries.iter() {
436 if *id == dist_id
437 && *ptr == series_id
438 && let Some(buf) = weak.upgrade()
439 {
440 return buf;
441 }
442 }
443
444 let buf = series.get_or_create_buf();
445 entries.push((dist_id, series_id, Arc::downgrade(&buf)));
446 buf
447 })
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454
455 #[test]
456 fn test_basic_recording() {
457 let dist = DynamicDistribution::new(4);
458 let labels = &[("org_id", "42")];
459
460 dist.record(labels, 100);
461 dist.record(labels, 200);
462 dist.record(labels, 300);
463
464 assert_eq!(dist.count(labels), 3);
465 assert_eq!(dist.sum(labels), 600);
466 }
467
468 #[test]
469 fn test_label_order_is_canonicalized() {
470 let dist = DynamicDistribution::new(4);
471
472 dist.record(&[("org_id", "42"), ("endpoint", "abc")], 100);
473
474 assert_eq!(dist.count(&[("endpoint", "abc"), ("org_id", "42")]), 1);
475 }
476
477 #[test]
478 fn test_series_handle() {
479 let dist = DynamicDistribution::new(4);
480 let series = dist.series(&[("org_id", "42")]);
481
482 series.record(100);
483 series.record(200);
484
485 assert_eq!(series.count(), 2);
486 assert_eq!(series.sum(), 300);
487 assert_eq!(dist.count(&[("org_id", "42")]), 2);
488 }
489
490 #[test]
491 fn test_multiple_label_sets() {
492 let dist = DynamicDistribution::new(4);
493
494 dist.record(&[("org_id", "1")], 100);
495 dist.record(&[("org_id", "2")], 200);
496
497 assert_eq!(dist.count(&[("org_id", "1")]), 1);
498 assert_eq!(dist.count(&[("org_id", "2")]), 1);
499
500 let snap = dist.snapshot();
501 assert_eq!(snap.len(), 2);
502 }
503
504 #[test]
505 fn test_overflow_bucket_routes_new_series_at_capacity() {
506 let dist = DynamicDistribution::with_max_series(4, 2);
507
508 dist.record(&[("org_id", "1")], 100);
509 dist.record(&[("org_id", "2")], 200);
510 dist.record(&[("org_id", "3")], 300);
512
513 assert_eq!(dist.cardinality(), 3); assert!(dist.overflow_count() > 0);
515 assert_eq!(dist.count(&[(OVERFLOW_LABEL_KEY, OVERFLOW_LABEL_VALUE)]), 1);
516 assert_eq!(dist.sum(&[(OVERFLOW_LABEL_KEY, OVERFLOW_LABEL_VALUE)]), 300);
517 }
518
519 #[test]
520 fn test_snapshot_includes_buckets() {
521 let dist = DynamicDistribution::new(4);
522 dist.record(&[("org_id", "1")], 100);
523 dist.record(&[("org_id", "1")], 200);
524
525 let snap = dist.snapshot();
526 assert_eq!(snap.len(), 1);
527 let (_, count, sum, bucket_snap) = &snap[0];
528 assert_eq!(*count, 2);
529 assert_eq!(*sum, 300);
530 assert!(bucket_snap.positive[6] > 0 || bucket_snap.positive[7] > 0);
532 }
533}