datadog_agent_commons/ipc/
session.rs

1//! IPC session management.
2
3use std::{
4    fmt,
5    sync::{Arc, Mutex},
6};
7
8use saluki_error::{ErrorContext as _, GenericError};
9use tokio::sync::Notify;
10use tonic::metadata::{Ascii, MetadataValue};
11
12/// Remote agent session ID.
13///
14/// Session IDs are acquired when registering as a remote agent with the Datadog Agent. This session ID must be provided
15/// when refreshing a remote agent's registration, as well as when streaming configuration updates. To ease passing this
16/// in the necessary format, and with the required validation, this struct provides a type-safe to carry around the
17/// session ID compared to a raw string.
18#[derive(Debug)]
19pub struct SessionId(MetadataValue<Ascii>);
20
21impl SessionId {
22    /// Creates a new `SessionId` from the given string.
23    ///
24    /// # Errors
25    ///
26    /// If the given string isn't valid ASCII, an error is returned.
27    pub fn new(session_id: &str) -> Result<Self, GenericError> {
28        MetadataValue::try_from(session_id)
29            .map(Self)
30            .error_context("Session ID is not valid ASCII")
31    }
32
33    /// Returns a reference to the string representation of the session ID.
34    pub fn as_str(&self) -> &str {
35        self.0
36            .to_str()
37            .expect("session ID is ensured to be valid ASCII on creation")
38    }
39
40    /// Returns the session ID as a metadata header value.
41    pub fn to_grpc_header_value(&self) -> MetadataValue<Ascii> {
42        self.0.clone()
43    }
44}
45
46impl fmt::Display for SessionId {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        write!(f, "{}", self.as_str())
49    }
50}
51
52#[derive(Debug, Default)]
53struct SessionIdHandleInner {
54    session_id: Mutex<Option<SessionId>>,
55    change_notify: Notify,
56}
57
58/// A handle for a dynamically updated session ID.
59///
60/// This handle allows sharing a session ID across multiple components while allowing it to be centrally updated in the
61/// case of refresh/re-registration.
62#[derive(Clone, Debug)]
63pub struct SessionIdHandle {
64    inner: Arc<SessionIdHandleInner>,
65}
66
67impl SessionIdHandle {
68    /// Creates a new `SessionIdHandle` with no session ID.
69    pub fn empty() -> Self {
70        Self {
71            inner: Arc::new(SessionIdHandleInner::default()),
72        }
73    }
74
75    /// Updates the current session ID to the given value.
76    pub fn update(&self, new_session_id: Option<SessionId>) {
77        if let Ok(mut session_id) = self.inner.session_id.lock() {
78            *session_id = new_session_id;
79            self.inner.change_notify.notify_waiters();
80        }
81    }
82
83    /// Gets the current session ID.
84    pub fn get(&self) -> Option<SessionId> {
85        self.inner
86            .session_id
87            .lock()
88            .ok()
89            .and_then(|s| (*s).as_ref().map(|session_id| SessionId(session_id.0.clone())))
90    }
91
92    /// Waits until the session ID is set to a non-empty value and returns it.
93    pub async fn wait_for_update(&self) -> SessionId {
94        loop {
95            let updated = self.inner.change_notify.notified();
96            if let Some(session_id) = self.get() {
97                return session_id;
98            }
99
100            updated.await;
101        }
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use std::time::Duration;
108
109    use tokio::time::timeout;
110
111    use super::{SessionId, SessionIdHandle};
112
113    // Bound every await so that a regression reintroducing the lost-wakeup race fails fast instead of
114    // hanging the test suite forever.
115    const TEST_TIMEOUT: Duration = Duration::from_secs(5);
116
117    #[tokio::test]
118    async fn wait_for_update_returns_immediately_when_value_already_set() {
119        // When a value is already present, `wait_for_update` must return it on the first loop iteration
120        // without ever awaiting a change notification.
121        let handle = SessionIdHandle::empty();
122        handle.update(Some(SessionId::new("already-set").unwrap()));
123
124        let session_id = timeout(TEST_TIMEOUT, handle.wait_for_update())
125            .await
126            .expect("should return without waiting on a notification");
127        assert_eq!(session_id.as_str(), "already-set");
128    }
129
130    #[tokio::test]
131    async fn wait_for_update_wakes_on_concurrent_update() {
132        // This exercises the documented race-avoidance idiom: `wait_for_update` registers the change
133        // notification *before* it reads the current value, so an update applied while a caller is parked
134        // is delivered rather than lost.
135        let handle = SessionIdHandle::empty();
136        let waiter = handle.clone();
137        let waiter_task = tokio::spawn(async move { waiter.wait_for_update().await });
138
139        // Let the spawned task advance to (and park on) its notification await while the value is still
140        // empty, then apply the update.
141        tokio::task::yield_now().await;
142        handle.update(Some(SessionId::new("session-123").unwrap()));
143
144        let session_id = timeout(TEST_TIMEOUT, waiter_task)
145            .await
146            .expect("waiter should wake within the timeout")
147            .expect("waiter task should not panic");
148        assert_eq!(session_id.as_str(), "session-123");
149    }
150
151    #[tokio::test]
152    async fn wait_for_update_ignores_clears_and_returns_first_non_empty_value() {
153        // `update(None)` still notifies waiters, but `wait_for_update` documents that it waits for a
154        // *non-empty* value, so a clear must not cause it to return early — it must keep waiting until a
155        // real session ID arrives.
156        let handle = SessionIdHandle::empty();
157        let waiter = handle.clone();
158        let waiter_task = tokio::spawn(async move { waiter.wait_for_update().await });
159
160        tokio::task::yield_now().await;
161        // Notifies waiters, but leaves the value empty: the waiter must loop and keep waiting.
162        handle.update(None);
163        tokio::task::yield_now().await;
164        handle.update(Some(SessionId::new("finally").unwrap()));
165
166        let session_id = timeout(TEST_TIMEOUT, waiter_task)
167            .await
168            .expect("waiter should wake within the timeout")
169            .expect("waiter task should not panic");
170        assert_eq!(session_id.as_str(), "finally");
171    }
172}