Skip to main content

libdd_common/
mutable_metadata.rs

1// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3
4//! Runtime metadata that the host tracer may update after startup.
5//!
6//! `runtime_id` and `process_tags` are the two tracer metadata values that may change
7//! while exporters and workers are already running. They live in a single
8//! [`MutableMetadata`] struct shared through an [`ArcSwap`]; every component holds a
9//! [`MutableMetadataHandle`] and reads the values through it at use time, so an update
10//! by the host SDK propagates to all components.
11//!
12//! # Protocol
13//!
14//! - **Writers** (the host SDK via FFI, or the library itself) call
15//!   [`MutableMetadataHandle::set_runtime_id`] / [`MutableMetadataHandle::set_process_tags`] /
16//!   [`MutableMetadataHandle::update`] to modify the current snapshot.
17//! - **Readers** call [`MutableMetadataHandle::load`] at the point of use, which returns a
18//!   lock-free snapshot. The snapshot is consistent for a single read only; values are **not**
19//!   consistent across multiple `load()` calls.
20
21use alloc::sync::Arc;
22
23use arc_swap::ArcSwap;
24
25/// The tracer metadata values that may be updated after startup.
26///
27/// Both fields use the same formats as the rest of the tracing stack:
28/// - `runtime_id`: a runtime UUID,
29/// - `process_tags`: comma-separated `key:value` pairs, e.g. `"k1:v1,k2:v2"`.
30#[derive(Clone, Default, Debug, PartialEq, Eq)]
31#[non_exhaustive]
32pub struct MutableMetadata {
33    /// Runtime UUID. Uniquely identifies the runtime within the host process.
34    pub runtime_id: String,
35    /// Comma-separated `key:value` process tags of the instrumented application.
36    pub process_tags: String,
37}
38
39/// A shared handle to a [`MutableMetadata`].
40///
41/// This is a cheap-to-clone `Arc<ArcSwap<MutableMetadata>>`: all clones observe the same
42/// underlying value. See the
43/// [module documentation](self) for the write/read protocol.
44#[derive(Clone, Default, Debug)]
45pub struct MutableMetadataHandle(Arc<ArcSwap<MutableMetadata>>);
46
47impl MutableMetadataHandle {
48    /// Load the value of the handle
49    pub fn load(&self) -> arc_swap::Guard<Arc<MutableMetadata>> {
50        self.0.load()
51    }
52
53    /// Load the value of the handle, cloned into an owning [`Arc`].
54    pub fn load_full(&self) -> Arc<MutableMetadata> {
55        self.0.load_full()
56    }
57
58    /// Set the runtime_id value
59    pub fn set_runtime_id(&self, runtime_id: String) {
60        self.update(|mut metadata| {
61            metadata.runtime_id = runtime_id.clone();
62            metadata
63        });
64    }
65
66    /// Set the process tags value
67    pub fn set_process_tags(&self, process_tags: String) {
68        self.update(|mut metadata| {
69            metadata.process_tags = process_tags.clone();
70            metadata
71        });
72    }
73
74    /// Update the handle value with the result of `f`.
75    ///
76    /// Use this method to update multiple fields at once. Modify the supplied snapshot
77    /// rather than replacing it with a previously loaded value. The closure may run
78    /// more than once when another writer updates the handle concurrently.
79    pub fn update<F>(&self, mut f: F)
80    where
81        F: FnMut(MutableMetadata) -> MutableMetadata,
82    {
83        self.0.rcu(|current| {
84            let next = (**current).clone();
85            f(next)
86        });
87    }
88}
89
90impl From<MutableMetadata> for MutableMetadataHandle {
91    fn from(value: MutableMetadata) -> Self {
92        MutableMetadataHandle(Arc::new(ArcSwap::from_pointee(value)))
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    fn initial_handle() -> MutableMetadataHandle {
101        let handle = MutableMetadataHandle::default();
102        handle.set_runtime_id("rt-1".into());
103        handle.set_process_tags("k1:v1".into());
104        handle
105    }
106
107    #[test]
108    fn handle_propagates_updates_to_all_clones() {
109        let handle = initial_handle();
110        let clone = handle.clone();
111
112        // Readers see the initial values through both clones.
113        let snapshot = handle.load();
114        assert_eq!(snapshot.runtime_id, "rt-1");
115        assert_eq!(snapshot.process_tags, "k1:v1");
116        drop(snapshot);
117
118        // Updating through one clone propagates to the other.
119        clone.update(|mut metadata| {
120            metadata.runtime_id = "rt-2".into();
121            metadata.process_tags = "k2:v2".into();
122            metadata
123        });
124        let snapshot = handle.load();
125        assert_eq!(snapshot.runtime_id, "rt-2");
126        assert_eq!(snapshot.process_tags, "k2:v2");
127    }
128
129    #[test]
130    fn field_updates_keep_the_other_field() {
131        let handle = initial_handle();
132
133        handle.set_runtime_id("rt-2".into());
134        let snapshot = handle.load();
135        assert_eq!(snapshot.runtime_id, "rt-2");
136        assert_eq!(snapshot.process_tags, "k1:v1");
137        drop(snapshot);
138
139        handle.set_process_tags("k2:v2".into());
140        let snapshot = handle.load();
141        assert_eq!(snapshot.runtime_id, "rt-2");
142        assert_eq!(snapshot.process_tags, "k2:v2");
143    }
144
145    #[test]
146    fn default_handle_yields_empty_values() {
147        let handle = MutableMetadataHandle::default();
148        let snapshot = handle.load();
149        assert!(snapshot.runtime_id.is_empty());
150        assert!(snapshot.process_tags.is_empty());
151    }
152
153    #[test]
154    fn update_modifies_the_current_snapshot() {
155        let handle = initial_handle();
156
157        handle.update(|mut m| {
158            m.runtime_id = "rt-2".into();
159            m
160        });
161        let snapshot = handle.load();
162        assert_eq!(snapshot.runtime_id, "rt-2");
163        assert_eq!(snapshot.process_tags, "k1:v1");
164    }
165
166    #[test]
167    fn update_retries_without_losing_concurrent_changes() {
168        use core::sync::atomic::{AtomicBool, Ordering};
169        use std::sync::Barrier;
170
171        let handle = initial_handle();
172        let snapshot_loaded = Barrier::new(2);
173        let concurrent_update_done = Barrier::new(2);
174        let first_attempt = AtomicBool::new(true);
175
176        std::thread::scope(|scope| {
177            let writer = scope.spawn(|| {
178                handle.update(|mut metadata| {
179                    if first_attempt.swap(false, Ordering::SeqCst) {
180                        snapshot_loaded.wait();
181                        concurrent_update_done.wait();
182                    }
183                    metadata.runtime_id = "rt-2".into();
184                    metadata
185                });
186            });
187            snapshot_loaded.wait();
188            handle.set_process_tags("k2:v2".into());
189            concurrent_update_done.wait();
190            writer.join().unwrap();
191        });
192
193        let snapshot = handle.load();
194        assert_eq!(snapshot.runtime_id, "rt-2");
195        assert_eq!(snapshot.process_tags, "k2:v2");
196    }
197
198    #[test]
199    fn load_full_returns_an_owning_arc() {
200        let handle = initial_handle();
201        let snapshot = handle.load_full();
202        assert_eq!(snapshot.runtime_id, "rt-1");
203        assert_eq!(snapshot.process_tags, "k1:v1");
204    }
205}