Skip to main content

saluki_context/
context.rs

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/// A metric context.
15#[derive(Clone, Debug, Eq, Hash, PartialEq)]
16pub struct Context {
17    inner: Arc<ContextInner>,
18}
19
20impl Context {
21    /// Creates a new `Context` from the given static name.
22    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    /// Creates a new `Context` from the given static name and given static tags.
39    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    /// Creates a new `Context` from the given name and given tags.
60    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    /// Clones this context, and uses the given name for the cloned context.
77    pub fn with_name<S: Into<MetaString>>(&self, name: S) -> Self {
78        // Regenerate the context key to account for the new name.
79        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    /// Clones this context, and uses the given tags for the cloned context.
96    ///
97    /// The name and origin tags of this context are preserved.
98    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    /// Clones this context, and uses the given origin tags for the cloned context.
116    ///
117    /// The name and instrumented tags of this context are preserved.
118    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    /// Clones this context, replacing both instrumented tags and origin tags in a single allocation.
136    ///
137    /// Preferred over two separate `with_tags` / `with_origin_tags` calls when both sets need to
138    /// be replaced, as it halves the number of `Arc` allocations.
139    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    /// Returns the name of this context.
166    pub fn name(&self) -> &MetaString {
167        &self.inner.name
168    }
169
170    /// Returns the instrumented tags of this context.
171    pub fn tags(&self) -> &TagSet {
172        &self.inner.tags
173    }
174
175    /// Returns the origin tags of this context.
176    pub fn origin_tags(&self) -> &TagSet {
177        &self.inner.origin_tags
178    }
179
180    /// Mutates the instrumented tags of this context via a closure.
181    ///
182    /// Uses copy-on-write semantics: if this context shares its inner data with other clones, the
183    /// inner data is cloned first so that mutations do not affect other holders. If this context is
184    /// the sole owner, the mutation happens in place.
185    ///
186    /// The context key is automatically recomputed after the closure returns.
187    pub fn mutate_tags(&mut self, f: impl FnOnce(&mut TagSet)) {
188        self.mutate_inner(|inner| f(&mut inner.tags));
189    }
190
191    /// Mutates the origin tags of this context via a closure.
192    ///
193    /// Uses copy-on-write semantics: if this context shares its inner data with other clones, the
194    /// inner data is cloned first so that mutations do not affect other holders. If this context is
195    /// the sole owner, the mutation happens in place.
196    ///
197    /// The context key is automatically recomputed after the closure returns.
198    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    /// Mutates both instrumented tags and origin tags via a single closure.
203    ///
204    /// Uses copy-on-write semantics: if this context shares its inner data with other clones, the
205    /// inner data is cloned first so that mutations do not affect other holders. If this context is
206    /// the sole owner, the mutation happens in place.
207    ///
208    /// The context key is recomputed once after the closure returns.
209    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    /// Runs the given closure on the inner context data, recomputing the context key afterwards.
214    ///
215    /// When the inner context state is shared (we aren't the only ones with a strong reference), we clone the inner
216    /// data first to have our own copy. Otherwise, we modify the inner data in place.
217    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    /// Creates a lazy copy-on-write mutable view over this context's tag sets.
225    ///
226    /// The returned view supports mutations (e.g. [`retain_tags`][TagSetMutView::retain_tags])
227    /// without immediately triggering an `Arc` clone. The actual clone, mutation, and context key
228    /// recomputation only happen when [`TagSetMutView::finish`] is called, and only if changes
229    /// were actually recorded.
230    ///
231    /// `state` provides reusable scratch space for tracking pending changes. Holding a
232    /// long-lived [`TagSetMutViewState`] across calls amortizes any vector allocations.
233    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    /// Returns the size of this context in bytes.
238    ///
239    /// A context's size is the sum of the sizes of its fields and the size of the `Context` struct itself, and
240    /// includes:
241    /// - the context name
242    /// - the context tags (both instrumented and origin)
243    ///
244    /// Since origin tags can potentially be expensive to calculate, this method will cache the size of the origin tags
245    /// when this method is first called.
246    ///
247    /// Additionally, the value returned by this method does not compensate for externalities such as origin tags
248    /// potentially being shared by multiple contexts, or whether or not tags are inlined, interned, or heap
249    /// allocated. This means that the value returned is essentially the worst-case usage, and should be used as a rough
250    /// estimate.
251    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/// Reusable scratch space for [`TagSetMutView`] operations.
297///
298/// Holding a long-lived instance across calls amortizes bitset allocations. The bitsets are
299/// cleared automatically when the associated [`TagSetMutView`] is dropped.
300#[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    /// Creates a new, empty state.
311    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
323/// A lazy copy-on-write mutable view over a [`Context`]'s tag sets.
324///
325/// Operations on this view (e.g. [`retain_tags`][Self::retain_tags]) are recorded but not
326/// applied immediately. The actual `Arc` clone, mutation, and context key recomputation only
327/// occur when [`finish`][Self::finish] is called, and only if changes were recorded.
328pub struct TagSetMutView<'a, 'b> {
329    context: &'a mut Context,
330    state: &'b mut TagSetMutViewState,
331}
332
333impl<'a, 'b> TagSetMutView<'a, 'b> {
334    /// Scan instrumented tags with the given predicate.
335    ///
336    /// Tags for which `f` returns `false` are flagged for removal. This is a read-only scan;
337    /// no mutation occurs until [`finish`][Self::finish] is called.
338    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    /// Scan origin tags with the given predicate.
347    ///
348    /// Tags for which `f` returns `false` are flagged for removal. This is a read-only scan;
349    /// no mutation occurs until [`finish`][Self::finish] is called.
350    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    /// Apply all recorded changes and return the total number of tags affected.
359    ///
360    /// If no changes were recorded, this is a no-op: no `Arc` clone, no rehash, returns 0.
361    /// Otherwise, triggers `Arc::make_mut` on the context, applies the changes to both tag sets,
362    /// and recomputes the context key.
363    ///
364    /// Returns the number of tags removed.
365    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            // We're specifically detaching this context from the statistics of the resolver from which `self`
431            // originated, as we only want to track the statistics of the contexts created _directly_ through the
432            // resolver.
433            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        // TODO: Note about why we consider the hash good enough for equality.
447        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        // Check the check after `with_name` when there's both tags and no tags.
514        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        // Make sure the size of the context is correct with origin tags.
546        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        // They share the same Arc before mutation.
558        assert!(original.ptr_eq(&mutated));
559
560        mutated.mutate_tags(|tags| {
561            tags.insert_tag(Tag::from("service:web"));
562        });
563
564        // After mutation, they no longer share the same inner.
565        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        // Original is unchanged.
578        assert_eq!(original.tags().len(), 1);
579        assert!(original.tags().has_tag("env:prod"));
580        assert!(!original.tags().has_tag("service:web"));
581
582        // Mutated has both tags.
583        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        // Build a context and mutate it to add a tag.
591        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        // Build an equivalent context from scratch with both tags.
597        let expected = Context::from_static_parts("metric", &["env:prod", "service:web"]);
598
599        // The recomputed key should match a freshly-constructed context with the same state.
600        assert_eq!(mutated, expected);
601
602        // Modify a tag on the mutated context that _isn't_ shared with `expected` to ensure that there's no asymmetric
603        // equality logic.
604        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    // --- Helper for contexts with origin tags ---
628
629    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    // --- TagSetMutView ---
641
642    #[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        // Remove b:2 and c:3 (keep a:1).
727        view.retain_tags(|tag| tag.name() == "a");
728        // Remove origin:y (keep origin:x).
729        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        // Path A: direct mutation.
743        let mut direct = base.clone();
744        direct.mutate_tags(|tags| tags.retain(predicate));
745
746        // Path B: mut view.
747        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        // Path A: direct mutation.
765        let mut direct = base.clone();
766        direct.mutate_origin_tags(|tags| tags.retain(predicate));
767
768        // Path B: mut view.
769        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        // Original is unchanged.
790        assert_eq!(original.tags().len(), 2);
791        assert!(original.tags().has_tag("env:prod"));
792        assert!(original.tags().has_tag("service:web"));
793
794        // Mutated has only the retained tag.
795        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); // Flag all for removal.
808                                         // Drop without calling finish().
809        }
810
811        // Nothing changed.
812        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        // First operation.
821        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        // Second operation reusing the same state.
831        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        // Start with a base tag, then add one via mutation to create an overlay.
844        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        // Two retain calls that both reject the same addition tag must not panic.
898        // Semantics: a tag survives only if ALL predicates accept it.
899        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        // First predicate removes "added:a" (keeps base:tag and added:b).
909        view.retain_tags(|tag| tag.as_str() != "added:a");
910        // Second predicate removes "base:tag" (keeps added:a and added:b).
911        // Combined effect: only "added:b" survives both predicates.
912        // "added:a" is flagged by both calls -- its duplicate index must be deduplicated.
913        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        // Both predicates reject "origin:c".
928        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        // Path A: two separate retain calls.
942        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        // Path B: single combined predicate.
950        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        // Path C: direct mutation.
957        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}