1use std::{fmt, hash, sync::Arc};
2
3use metrics::Gauge;
4use saluki_common::collections::{ContiguousBitSet, PrehashedHashSet};
5use stringtheory::MetaString;
6
7use crate::{
8 hash::{hash_context, hash_context_with_seen, ContextKey},
9 tags::{Tag, TagSet},
10};
11
12const BASE_CONTEXT_SIZE: usize = std::mem::size_of::<Context>() + std::mem::size_of::<ContextInner>();
13
14#[derive(Clone, Debug, Eq, Hash, PartialEq)]
16pub struct Context {
17 inner: Arc<ContextInner>,
18}
19
20impl Context {
21 pub fn from_static_name(name: &'static str) -> Self {
23 let tags = TagSet::default();
24 let origin_tags = TagSet::default();
25
26 let (key, _) = hash_context(name, &tags, &origin_tags);
27 Self {
28 inner: Arc::new(ContextInner {
29 name: MetaString::from_static(name),
30 tags,
31 origin_tags,
32 key,
33 active_count: Gauge::noop(),
34 }),
35 }
36 }
37
38 pub fn from_static_parts(name: &'static str, tags: &[&'static str]) -> Self {
40 let mut tag_set = TagSet::with_capacity(tags.len());
41 for tag in tags {
42 tag_set.insert_tag(MetaString::from_static(tag));
43 }
44
45 let origin_tags = TagSet::default();
46
47 let (key, _) = hash_context(name, &tag_set, &origin_tags);
48 Self {
49 inner: Arc::new(ContextInner {
50 name: MetaString::from_static(name),
51 tags: tag_set,
52 origin_tags,
53 key,
54 active_count: Gauge::noop(),
55 }),
56 }
57 }
58
59 pub fn from_parts<S: Into<MetaString>>(name: S, tags: impl Into<TagSet>) -> Self {
61 let name = name.into();
62 let tags = tags.into();
63 let origin_tags = TagSet::default();
64 let (key, _) = hash_context(&name, &tags, &origin_tags);
65 Self {
66 inner: Arc::new(ContextInner {
67 name,
68 tags,
69 origin_tags,
70 key,
71 active_count: Gauge::noop(),
72 }),
73 }
74 }
75
76 pub fn with_name<S: Into<MetaString>>(&self, name: S) -> Self {
78 let name = name.into();
80 let tags = self.inner.tags.clone();
81 let origin_tags = self.inner.origin_tags.clone();
82 let (key, _) = hash_context(&name, &tags, &origin_tags);
83
84 Self {
85 inner: Arc::new(ContextInner {
86 name,
87 tags,
88 origin_tags,
89 key,
90 active_count: Gauge::noop(),
91 }),
92 }
93 }
94
95 pub fn with_tags(&self, tags: impl Into<TagSet>) -> Self {
99 let name = self.inner.name.clone();
100 let tags = tags.into();
101 let origin_tags = self.inner.origin_tags.clone();
102 let (key, _) = hash_context(&name, &tags, &origin_tags);
103
104 Self {
105 inner: Arc::new(ContextInner {
106 name,
107 tags,
108 origin_tags,
109 key,
110 active_count: Gauge::noop(),
111 }),
112 }
113 }
114
115 pub fn with_origin_tags(&self, origin_tags: impl Into<TagSet>) -> Self {
119 let name = self.inner.name.clone();
120 let tags = self.inner.tags.clone();
121 let origin_tags = origin_tags.into();
122 let (key, _) = hash_context(&name, &tags, &origin_tags);
123
124 Self {
125 inner: Arc::new(ContextInner {
126 name,
127 tags,
128 origin_tags,
129 key,
130 active_count: Gauge::noop(),
131 }),
132 }
133 }
134
135 pub fn with_tags_and_origin_tags(&self, tags: impl Into<TagSet>, origin_tags: impl Into<TagSet>) -> Self {
140 let name = self.inner.name.clone();
141 let tags = tags.into();
142 let origin_tags = origin_tags.into();
143 let (key, _) = hash_context(&name, &tags, &origin_tags);
144
145 Self {
146 inner: Arc::new(ContextInner {
147 name,
148 tags,
149 origin_tags,
150 key,
151 active_count: Gauge::noop(),
152 }),
153 }
154 }
155
156 pub(crate) fn from_inner(inner: ContextInner) -> Self {
157 Self { inner: Arc::new(inner) }
158 }
159
160 #[cfg(test)]
161 pub(crate) fn ptr_eq(&self, other: &Self) -> bool {
162 Arc::ptr_eq(&self.inner, &other.inner)
163 }
164
165 pub fn name(&self) -> &MetaString {
167 &self.inner.name
168 }
169
170 pub fn tags(&self) -> &TagSet {
172 &self.inner.tags
173 }
174
175 pub fn origin_tags(&self) -> &TagSet {
177 &self.inner.origin_tags
178 }
179
180 pub fn mutate_tags(&mut self, f: impl FnOnce(&mut TagSet)) {
188 self.mutate_inner(|inner| f(&mut inner.tags));
189 }
190
191 pub fn mutate_origin_tags(&mut self, f: impl FnOnce(&mut TagSet)) {
199 self.mutate_inner(|inner| f(&mut inner.origin_tags));
200 }
201
202 pub fn with_tag_sets_mut(&mut self, f: impl FnOnce(&mut TagSet, &mut TagSet)) {
210 self.mutate_inner(|inner| f(&mut inner.tags, &mut inner.origin_tags));
211 }
212
213 fn mutate_inner(&mut self, f: impl FnOnce(&mut ContextInner)) {
218 let inner = Arc::make_mut(&mut self.inner);
219 f(inner);
220 let (key, _) = hash_context(&inner.name, &inner.tags, &inner.origin_tags);
221 inner.key = key;
222 }
223
224 pub fn tags_mut_view<'a, 'b>(&'a mut self, state: &'b mut TagSetMutViewState) -> TagSetMutView<'a, 'b> {
234 TagSetMutView { context: self, state }
235 }
236
237 pub fn size_of(&self) -> usize {
252 let name_size = self.inner.name.len();
253 let tags_size = self.inner.tags.size_of();
254 let origin_tags_size = self.inner.origin_tags.size_of();
255
256 BASE_CONTEXT_SIZE + name_size + tags_size + origin_tags_size
257 }
258}
259
260impl From<&'static str> for Context {
261 fn from(name: &'static str) -> Self {
262 Self::from_static_name(name)
263 }
264}
265
266impl<'a> From<(&'static str, &'a [&'static str])> for Context {
267 fn from((name, tags): (&'static str, &'a [&'static str])) -> Self {
268 Self::from_static_parts(name, tags)
269 }
270}
271
272impl fmt::Display for Context {
273 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274 write!(f, "{}", self.inner.name)?;
275 if !self.inner.tags.is_empty() {
276 write!(f, "{{")?;
277
278 let mut needs_separator = false;
279 for tag in &self.inner.tags {
280 if needs_separator {
281 write!(f, ", ")?;
282 } else {
283 needs_separator = true;
284 }
285
286 write!(f, "{}", tag)?;
287 }
288
289 write!(f, "}}")?;
290 }
291
292 Ok(())
293 }
294}
295
296#[derive(Debug, Default)]
301pub struct TagSetMutViewState {
302 tag_base_removals: ContiguousBitSet,
303 tag_addition_removals: ContiguousBitSet,
304 origin_base_removals: ContiguousBitSet,
305 origin_addition_removals: ContiguousBitSet,
306 hash_seen: PrehashedHashSet<u64>,
307}
308
309impl TagSetMutViewState {
310 pub fn new() -> Self {
312 Self::default()
313 }
314
315 fn clear(&mut self) {
316 self.tag_base_removals.clear_all();
317 self.tag_addition_removals.clear_all();
318 self.origin_base_removals.clear_all();
319 self.origin_addition_removals.clear_all();
320 }
321}
322
323pub struct TagSetMutView<'a, 'b> {
329 context: &'a mut Context,
330 state: &'b mut TagSetMutViewState,
331}
332
333impl<'a, 'b> TagSetMutView<'a, 'b> {
334 pub fn retain_tags(&mut self, f: impl FnMut(&Tag) -> bool) {
339 self.context.inner.tags.collect_removals(
340 f,
341 &mut self.state.tag_base_removals,
342 &mut self.state.tag_addition_removals,
343 );
344 }
345
346 pub fn retain_origin_tags(&mut self, f: impl FnMut(&Tag) -> bool) {
351 self.context.inner.origin_tags.collect_removals(
352 f,
353 &mut self.state.origin_base_removals,
354 &mut self.state.origin_addition_removals,
355 );
356 }
357
358 pub fn finish(self) -> usize {
366 let total_tags = self.state.tag_base_removals.len() + self.state.tag_addition_removals.len();
367 let total_origin = self.state.origin_base_removals.len() + self.state.origin_addition_removals.len();
368 let total = total_tags + total_origin;
369
370 if total == 0 {
371 return 0;
372 }
373
374 let inner = Arc::make_mut(&mut self.context.inner);
375
376 if total_tags > 0 {
377 inner
378 .tags
379 .apply_removals(&self.state.tag_base_removals, &self.state.tag_addition_removals);
380 }
381 if total_origin > 0 {
382 inner
383 .origin_tags
384 .apply_removals(&self.state.origin_base_removals, &self.state.origin_addition_removals);
385 }
386
387 let (key, _) = hash_context_with_seen(&inner.name, &inner.tags, &inner.origin_tags, &mut self.state.hash_seen);
388 inner.key = key;
389
390 total
391 }
392}
393
394impl Drop for TagSetMutView<'_, '_> {
395 fn drop(&mut self) {
396 self.state.clear();
397 }
398}
399
400pub(super) struct ContextInner {
401 key: ContextKey,
402 name: MetaString,
403 tags: TagSet,
404 origin_tags: TagSet,
405 active_count: Gauge,
406}
407
408impl ContextInner {
409 pub fn from_parts(
410 key: ContextKey, name: MetaString, tags: TagSet, origin_tags: TagSet, active_count: Gauge,
411 ) -> Self {
412 Self {
413 key,
414 name,
415 tags,
416 origin_tags,
417 active_count,
418 }
419 }
420}
421
422impl Clone for ContextInner {
423 fn clone(&self) -> Self {
424 Self {
425 key: self.key,
426 name: self.name.clone(),
427 tags: self.tags.clone(),
428 origin_tags: self.origin_tags.clone(),
429
430 active_count: Gauge::noop(),
434 }
435 }
436}
437
438impl Drop for ContextInner {
439 fn drop(&mut self) {
440 self.active_count.decrement(1);
441 }
442}
443
444impl PartialEq<ContextInner> for ContextInner {
445 fn eq(&self, other: &ContextInner) -> bool {
446 self.key == other.key
448 }
449}
450
451impl Eq for ContextInner {}
452
453impl hash::Hash for ContextInner {
454 fn hash<H: hash::Hasher>(&self, state: &mut H) {
455 self.key.hash(state);
456 }
457}
458
459impl fmt::Debug for ContextInner {
460 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
461 f.debug_struct("ContextInner")
462 .field("name", &self.name)
463 .field("tags", &self.tags)
464 .field("key", &self.key)
465 .finish()
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472 use crate::tags::Tag;
473
474 const SIZE_OF_CONTEXT_NAME: &str = "size_of_test_metric";
475 const SIZE_OF_CONTEXT_CHANGED_NAME: &str = "size_of_test_metric_changed";
476 const SIZE_OF_CONTEXT_TAGS: &[&str] = &["size_of_test_tag1", "size_of_test_tag2"];
477 const SIZE_OF_CONTEXT_ORIGIN_TAGS: &[&str] = &["size_of_test_origin_tag1", "size_of_test_origin_tag2"];
478
479 fn tag_set(tags: &[&str]) -> TagSet {
480 tags.iter().map(|s| Tag::from(*s)).collect::<TagSet>()
481 }
482
483 #[test]
484 fn size_of_context_from_static_name() {
485 let context = Context::from_static_name(SIZE_OF_CONTEXT_NAME);
486 assert_eq!(context.size_of(), BASE_CONTEXT_SIZE + SIZE_OF_CONTEXT_NAME.len());
487 }
488
489 #[test]
490 fn size_of_context_from_static_parts() {
491 let tags = tag_set(SIZE_OF_CONTEXT_TAGS);
492
493 let context = Context::from_static_parts(SIZE_OF_CONTEXT_NAME, SIZE_OF_CONTEXT_TAGS);
494 assert_eq!(
495 context.size_of(),
496 BASE_CONTEXT_SIZE + SIZE_OF_CONTEXT_NAME.len() + tags.size_of()
497 );
498 }
499
500 #[test]
501 fn size_of_context_from_parts() {
502 let tags = tag_set(SIZE_OF_CONTEXT_TAGS);
503
504 let context = Context::from_parts(SIZE_OF_CONTEXT_NAME, tags.clone());
505 assert_eq!(
506 context.size_of(),
507 BASE_CONTEXT_SIZE + SIZE_OF_CONTEXT_NAME.len() + tags.size_of()
508 );
509 }
510
511 #[test]
512 fn size_of_context_with_name() {
513 let context = Context::from_static_name(SIZE_OF_CONTEXT_NAME).with_name(SIZE_OF_CONTEXT_CHANGED_NAME);
515 assert_eq!(
516 context.size_of(),
517 BASE_CONTEXT_SIZE + SIZE_OF_CONTEXT_CHANGED_NAME.len()
518 );
519
520 let tags = tag_set(SIZE_OF_CONTEXT_TAGS);
521
522 let context = Context::from_static_parts(SIZE_OF_CONTEXT_NAME, SIZE_OF_CONTEXT_TAGS)
523 .with_name(SIZE_OF_CONTEXT_CHANGED_NAME);
524 assert_eq!(
525 context.size_of(),
526 BASE_CONTEXT_SIZE + SIZE_OF_CONTEXT_CHANGED_NAME.len() + tags.size_of()
527 );
528 }
529
530 #[test]
531 fn size_of_context_origin_tags() {
532 let tags = tag_set(SIZE_OF_CONTEXT_TAGS);
533 let origin_tags = tag_set(SIZE_OF_CONTEXT_ORIGIN_TAGS);
534
535 let (key, _) = hash_context(SIZE_OF_CONTEXT_NAME, SIZE_OF_CONTEXT_TAGS, SIZE_OF_CONTEXT_ORIGIN_TAGS);
536
537 let context = Context::from_inner(ContextInner {
538 key,
539 name: MetaString::from_static(SIZE_OF_CONTEXT_NAME),
540 tags: tags.clone(),
541 origin_tags: origin_tags.clone(),
542 active_count: Gauge::noop(),
543 });
544
545 assert_eq!(
547 context.size_of(),
548 BASE_CONTEXT_SIZE + SIZE_OF_CONTEXT_NAME.len() + tags.size_of() + origin_tags.size_of()
549 );
550 }
551
552 #[test]
553 fn with_tags_mut_clones_shared_context() {
554 let original = Context::from_static_parts("metric", &["env:prod"]);
555 let mut mutated = original.clone();
556
557 assert!(original.ptr_eq(&mutated));
559
560 mutated.mutate_tags(|tags| {
561 tags.insert_tag(Tag::from("service:web"));
562 });
563
564 assert!(!original.ptr_eq(&mutated));
566 }
567
568 #[test]
569 fn with_tags_mut_does_not_affect_original() {
570 let original = Context::from_static_parts("metric", &["env:prod"]);
571 let mut mutated = original.clone();
572
573 mutated.mutate_tags(|tags| {
574 tags.insert_tag(Tag::from("service:web"));
575 });
576
577 assert_eq!(original.tags().len(), 1);
579 assert!(original.tags().has_tag("env:prod"));
580 assert!(!original.tags().has_tag("service:web"));
581
582 assert_eq!(mutated.tags().len(), 2);
584 assert!(mutated.tags().has_tag("env:prod"));
585 assert!(mutated.tags().has_tag("service:web"));
586 }
587
588 #[test]
589 fn with_tags_mut_rehashes() {
590 let mut mutated = Context::from_static_parts("metric", &["env:prod"]);
592 mutated.mutate_tags(|tags| {
593 tags.insert_tag(Tag::from("service:web"));
594 });
595
596 let expected = Context::from_static_parts("metric", &["env:prod", "service:web"]);
598
599 assert_eq!(mutated, expected);
601
602 mutated.mutate_tags(|tags| {
605 tags.insert_tag(Tag::from("cluster:foo"));
606 });
607 assert_ne!(mutated, expected);
608 }
609
610 #[test]
611 fn with_origin_tags_mut_clones_shared_context() {
612 let original = Context::from_static_name("metric");
613 let mut mutated = original.clone();
614
615 assert!(original.ptr_eq(&mutated));
616
617 mutated.mutate_origin_tags(|tags| {
618 tags.insert_tag(Tag::from("origin:tag"));
619 });
620
621 assert!(!original.ptr_eq(&mutated));
622 assert!(original.origin_tags().is_empty());
623 assert_eq!(mutated.origin_tags().len(), 1);
624 assert!(mutated.origin_tags().has_tag("origin:tag"));
625 }
626
627 fn context_with_origin(name: &'static str, tags: &[&'static str], origin_tags: &[&'static str]) -> Context {
630 let (key, _) = hash_context(name, tags, origin_tags);
631 Context::from_inner(ContextInner {
632 key,
633 name: MetaString::from_static(name),
634 tags: tag_set(tags),
635 origin_tags: tag_set(origin_tags),
636 active_count: Gauge::noop(),
637 })
638 }
639
640 #[test]
643 fn mut_view_retain_tags_removes_matching() {
644 let mut ctx = Context::from_static_parts("metric", &["env:prod", "service:web", "region:us"]);
645 let mut state = TagSetMutViewState::new();
646
647 let mut view = ctx.tags_mut_view(&mut state);
648 view.retain_tags(|tag| tag.name() == "env");
649 let removed = view.finish();
650
651 assert_eq!(removed, 2);
652 assert_eq!(ctx.tags().len(), 1);
653 assert!(ctx.tags().has_tag("env:prod"));
654 assert!(!ctx.tags().has_tag("service:web"));
655 assert!(!ctx.tags().has_tag("region:us"));
656 }
657
658 #[test]
659 fn mut_view_retain_origin_tags_removes_matching() {
660 let mut ctx = context_with_origin("metric", &[], &["origin:a", "origin:b", "origin:c"]);
661 let mut state = TagSetMutViewState::new();
662
663 let mut view = ctx.tags_mut_view(&mut state);
664 view.retain_origin_tags(|tag| tag.as_str() == "origin:a");
665 let removed = view.finish();
666
667 assert_eq!(removed, 2);
668 assert_eq!(ctx.origin_tags().len(), 1);
669 assert!(ctx.origin_tags().has_tag("origin:a"));
670 assert!(!ctx.origin_tags().has_tag("origin:b"));
671 assert!(!ctx.origin_tags().has_tag("origin:c"));
672 }
673
674 #[test]
675 fn mut_view_retain_both_tag_sets() {
676 let mut ctx = context_with_origin("metric", &["env:prod", "service:web"], &["origin:a", "origin:b"]);
677 let mut state = TagSetMutViewState::new();
678
679 let mut view = ctx.tags_mut_view(&mut state);
680 view.retain_tags(|tag| tag.name() == "env");
681 view.retain_origin_tags(|tag| tag.as_str() == "origin:a");
682 let removed = view.finish();
683
684 assert_eq!(removed, 2);
685 assert_eq!(ctx.tags().len(), 1);
686 assert!(ctx.tags().has_tag("env:prod"));
687 assert!(!ctx.tags().has_tag("service:web"));
688 assert_eq!(ctx.origin_tags().len(), 1);
689 assert!(ctx.origin_tags().has_tag("origin:a"));
690 assert!(!ctx.origin_tags().has_tag("origin:b"));
691 }
692
693 #[test]
694 fn mut_view_retain_all_is_noop() {
695 let original = Context::from_static_parts("metric", &["env:prod", "service:web"]);
696 let mut ctx = original.clone();
697 let mut state = TagSetMutViewState::new();
698
699 let mut view = ctx.tags_mut_view(&mut state);
700 view.retain_tags(|_| true);
701 let removed = view.finish();
702
703 assert_eq!(removed, 0);
704 assert!(ctx.ptr_eq(&original));
705 }
706
707 #[test]
708 fn mut_view_retain_none_removes_all() {
709 let mut ctx = Context::from_static_parts("metric", &["env:prod", "service:web", "region:us"]);
710 let mut state = TagSetMutViewState::new();
711
712 let mut view = ctx.tags_mut_view(&mut state);
713 view.retain_tags(|_| false);
714 let removed = view.finish();
715
716 assert_eq!(removed, 3);
717 assert!(ctx.tags().is_empty());
718 }
719
720 #[test]
721 fn mut_view_finish_returns_correct_count() {
722 let mut ctx = context_with_origin("metric", &["a:1", "b:2", "c:3"], &["origin:x", "origin:y"]);
723 let mut state = TagSetMutViewState::new();
724
725 let mut view = ctx.tags_mut_view(&mut state);
726 view.retain_tags(|tag| tag.name() == "a");
728 view.retain_origin_tags(|tag| tag.as_str() == "origin:x");
730 let removed = view.finish();
731
732 assert_eq!(removed, 3);
733 assert_eq!(ctx.tags().len(), 1);
734 assert_eq!(ctx.origin_tags().len(), 1);
735 }
736
737 #[test]
738 fn mut_view_equivalent_to_direct_mutate_tags() {
739 let base = Context::from_static_parts("metric", &["env:prod", "service:web", "region:us"]);
740 let predicate = |tag: &Tag| tag.name() == "env";
741
742 let mut direct = base.clone();
744 direct.mutate_tags(|tags| tags.retain(predicate));
745
746 let mut via_view = base.clone();
748 let mut state = TagSetMutViewState::new();
749 let mut view = via_view.tags_mut_view(&mut state);
750 view.retain_tags(predicate);
751 view.finish();
752
753 assert_eq!(direct, via_view);
754 assert_eq!(direct.tags().len(), via_view.tags().len());
755 assert!(via_view.tags().has_tag("env:prod"));
756 assert!(!via_view.tags().has_tag("service:web"));
757 }
758
759 #[test]
760 fn mut_view_equivalent_to_direct_mutate_origin_tags() {
761 let base = context_with_origin("metric", &["env:prod"], &["origin:a", "origin:b", "origin:c"]);
762 let predicate = |tag: &Tag| tag.as_str() == "origin:a";
763
764 let mut direct = base.clone();
766 direct.mutate_origin_tags(|tags| tags.retain(predicate));
767
768 let mut via_view = base.clone();
770 let mut state = TagSetMutViewState::new();
771 let mut view = via_view.tags_mut_view(&mut state);
772 view.retain_origin_tags(predicate);
773 view.finish();
774
775 assert_eq!(direct, via_view);
776 assert_eq!(direct.origin_tags().len(), via_view.origin_tags().len());
777 }
778
779 #[test]
780 fn mut_view_does_not_affect_cloned_context() {
781 let original = Context::from_static_parts("metric", &["env:prod", "service:web"]);
782 let mut mutated = original.clone();
783 let mut state = TagSetMutViewState::new();
784
785 let mut view = mutated.tags_mut_view(&mut state);
786 view.retain_tags(|tag| tag.name() == "env");
787 view.finish();
788
789 assert_eq!(original.tags().len(), 2);
791 assert!(original.tags().has_tag("env:prod"));
792 assert!(original.tags().has_tag("service:web"));
793
794 assert_eq!(mutated.tags().len(), 1);
796 assert!(!original.ptr_eq(&mutated));
797 }
798
799 #[test]
800 fn mut_view_drop_without_finish_discards_changes() {
801 let original = Context::from_static_parts("metric", &["env:prod", "service:web"]);
802 let mut ctx = original.clone();
803 let mut state = TagSetMutViewState::new();
804
805 {
806 let mut view = ctx.tags_mut_view(&mut state);
807 view.retain_tags(|_| false); }
810
811 assert_eq!(ctx.tags().len(), 2);
813 assert!(ctx.ptr_eq(&original));
814 }
815
816 #[test]
817 fn mut_view_state_reuse_across_operations() {
818 let mut state = TagSetMutViewState::new();
819
820 let mut ctx1 = Context::from_static_parts("metric1", &["a:1", "b:2"]);
822 let mut view1 = ctx1.tags_mut_view(&mut state);
823 view1.retain_tags(|tag| tag.name() == "a");
824 let removed1 = view1.finish();
825
826 assert_eq!(removed1, 1);
827 assert_eq!(ctx1.tags().len(), 1);
828 assert!(ctx1.tags().has_tag("a:1"));
829
830 let mut ctx2 = Context::from_static_parts("metric2", &["x:1", "y:2", "z:3"]);
832 let mut view2 = ctx2.tags_mut_view(&mut state);
833 view2.retain_tags(|tag| tag.name() == "z");
834 let removed2 = view2.finish();
835
836 assert_eq!(removed2, 2);
837 assert_eq!(ctx2.tags().len(), 1);
838 assert!(ctx2.tags().has_tag("z:3"));
839 }
840
841 #[test]
842 fn mut_view_retain_tags_with_additions() {
843 let mut ctx = Context::from_static_parts("metric", &["base:tag"]);
845 ctx.mutate_tags(|tags| {
846 tags.insert_tag(Tag::from("added:tag"));
847 });
848 assert_eq!(ctx.tags().len(), 2);
849
850 let mut state = TagSetMutViewState::new();
851 let mut view = ctx.tags_mut_view(&mut state);
852 view.retain_tags(|tag| tag.name() == "added");
853 let removed = view.finish();
854
855 assert_eq!(removed, 1);
856 assert_eq!(ctx.tags().len(), 1);
857 assert!(ctx.tags().has_tag("added:tag"));
858 assert!(!ctx.tags().has_tag("base:tag"));
859 }
860
861 #[test]
862 fn mut_view_retain_tags_removes_only_additions() {
863 let mut ctx = Context::from_static_parts("metric", &["base:tag"]);
864 ctx.mutate_tags(|tags| {
865 tags.insert_tag(Tag::from("added:tag"));
866 });
867
868 let mut state = TagSetMutViewState::new();
869 let mut view = ctx.tags_mut_view(&mut state);
870 view.retain_tags(|tag| tag.name() == "base");
871 let removed = view.finish();
872
873 assert_eq!(removed, 1);
874 assert_eq!(ctx.tags().len(), 1);
875 assert!(ctx.tags().has_tag("base:tag"));
876 assert!(!ctx.tags().has_tag("added:tag"));
877 }
878
879 #[test]
880 fn mut_view_retain_tags_removes_base_and_additions() {
881 let mut ctx = Context::from_static_parts("metric", &["base:tag"]);
882 ctx.mutate_tags(|tags| {
883 tags.insert_tag(Tag::from("added:tag"));
884 });
885
886 let mut state = TagSetMutViewState::new();
887 let mut view = ctx.tags_mut_view(&mut state);
888 view.retain_tags(|_| false);
889 let removed = view.finish();
890
891 assert_eq!(removed, 2);
892 assert!(ctx.tags().is_empty());
893 }
894
895 #[test]
896 fn mut_view_multiple_retain_calls_deduplicates() {
897 let mut ctx = Context::from_static_parts("metric", &["base:tag"]);
900 ctx.mutate_tags(|tags| {
901 tags.insert_tag(Tag::from("added:a"));
902 tags.insert_tag(Tag::from("added:b"));
903 });
904 assert_eq!(ctx.tags().len(), 3);
905
906 let mut state = TagSetMutViewState::new();
907 let mut view = ctx.tags_mut_view(&mut state);
908 view.retain_tags(|tag| tag.as_str() != "added:a");
910 view.retain_tags(|tag| tag.name() != "base");
914 let removed = view.finish();
915
916 assert_eq!(removed, 2);
917 assert_eq!(ctx.tags().len(), 1);
918 assert!(ctx.tags().has_tag("added:b"));
919 }
920
921 #[test]
922 fn mut_view_multiple_retain_origin_calls_deduplicates() {
923 let mut ctx = context_with_origin("metric", &[], &["origin:a", "origin:b", "origin:c"]);
924 let mut state = TagSetMutViewState::new();
925
926 let mut view = ctx.tags_mut_view(&mut state);
927 view.retain_origin_tags(|tag| tag.as_str() != "origin:c");
929 view.retain_origin_tags(|tag| tag.as_str() == "origin:a");
930 let removed = view.finish();
931
932 assert_eq!(removed, 2);
933 assert_eq!(ctx.origin_tags().len(), 1);
934 assert!(ctx.origin_tags().has_tag("origin:a"));
935 }
936
937 #[test]
938 fn mut_view_multiple_retain_equivalent_to_combined_predicate() {
939 let base = Context::from_static_parts("metric", &["env:prod", "service:web", "region:us", "cluster:main"]);
940
941 let mut via_two = base.clone();
943 let mut state = TagSetMutViewState::new();
944 let mut view = via_two.tags_mut_view(&mut state);
945 view.retain_tags(|tag| tag.name() != "region");
946 view.retain_tags(|tag| tag.name() != "cluster");
947 view.finish();
948
949 let mut via_one = base.clone();
951 let mut state2 = TagSetMutViewState::new();
952 let mut view2 = via_one.tags_mut_view(&mut state2);
953 view2.retain_tags(|tag| tag.name() != "region" && tag.name() != "cluster");
954 view2.finish();
955
956 let mut via_direct = base.clone();
958 via_direct.mutate_tags(|tags| tags.retain(|tag| tag.name() != "region" && tag.name() != "cluster"));
959
960 assert_eq!(via_two, via_one);
961 assert_eq!(via_two, via_direct);
962 }
963}