Skip to main content

synapto_interface/
sync.rs

1//! # Transparent Sync Proxy
2//!
3//! This module provides a drop-in replacement for `tokio::sync` primitives with built-in
4//! telemetry instrumentation for Rerun pulses.
5//!
6//! ## Architecture
7//!
8//! - **Release Builds**: All types and functions are simple re-exports of `tokio::sync`.
9//!   There is **absolute zero runtime cost**.
10//! - **Debug Builds**: `mpsc` and `broadcast` channels are shadowed by instrumented wrappers.
11//!   Every successful `send` or `try_send` emits a `tracing::trace!` pulse that is
12//!   automatically captured by the core telemetry system.
13//!
14//! ## Automatic Naming
15//!
16//! Metrics are automatically named using the following format:
17//! `semantic_name:channel_type:message_type@module_path`
18//!
19//! - **semantic_name**: Name provided via mandatory `register_channel_name!`.
20//! - **channel_type**: `mpsc`, `unbounded`, or `broadcast`.
21//! - **message_type**: Inferred from the generic type `T`.
22//! - **module_path**: Inferred from the caller's location using `#[track_caller]`.
23
24#[cfg(not(debug_assertions))]
25pub use tokio::sync::*;
26
27#[cfg(debug_assertions)]
28pub mod mpsc {
29    //! Instrumented `mpsc` channels.
30    use std::ops::Deref;
31    use tokio::sync::mpsc::error::{SendError, TrySendError};
32    pub use tokio::sync::mpsc::{Receiver, UnboundedReceiver, error};
33
34    /// Creates a bounded mpsc channel for communicating between asynchronous tasks.
35    ///
36    /// The returned `Sender` is instrumented with automatic telemetry pulses.
37    /// The message type `T` must be registered via `register_channel_name!`.
38    #[track_caller]
39    pub fn channel<T: crate::sync::TypeChannelName>(buffer: usize) -> (Sender<T>, Receiver<T>) {
40        let (tx, rx) = tokio::sync::mpsc::channel(buffer);
41        (Sender(tx), rx)
42    }
43
44    #[track_caller]
45    pub fn unbounded_channel<T: crate::sync::TypeChannelName>()
46    -> (UnboundedSender<T>, UnboundedReceiver<T>) {
47        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
48        (UnboundedSender(tx), rx)
49    }
50
51    /// An instrumented wrapper around `tokio::sync::mpsc::Sender`.
52    #[derive(Debug)]
53    #[repr(transparent)]
54    pub struct Sender<T: crate::sync::TypeChannelName>(tokio::sync::mpsc::Sender<T>);
55
56    impl<T: crate::sync::TypeChannelName> Clone for Sender<T> {
57        fn clone(&self) -> Self {
58            Self(self.0.clone())
59        }
60    }
61
62    impl<T: crate::sync::TypeChannelName> Sender<T> {
63        /// Sends a value, waiting until there is capacity.
64        ///
65        /// Automatically emits a telemetry pulse upon successful send.
66        // TODO Remove lint exception after https://github.com/rust-lang/rust/issues/110011
67        #[allow(ungated_async_fn_track_caller)]
68        #[track_caller]
69        pub async fn send(&self, value: T) -> Result<(), SendError<T>> {
70            let res = self.0.send(value).await;
71            if res.is_ok() {
72                super::detail::trace_pulse::<T>("mpsc", std::panic::Location::caller());
73            }
74            res
75        }
76
77        /// Sends a value, blocking the current thread until there is capacity.
78        ///
79        /// Automatically emits a telemetry pulse upon successful send.
80        #[track_caller]
81        pub fn blocking_send(&self, value: T) -> Result<(), SendError<T>> {
82            let res = self.0.blocking_send(value);
83            if res.is_ok() {
84                super::detail::trace_pulse::<T>("mpsc", std::panic::Location::caller());
85            }
86            res
87        }
88
89        /// Attempts to send a value without waiting for capacity.
90        ///
91        /// Automatically emits a telemetry pulse upon successful send.
92        #[track_caller]
93        pub fn try_send(&self, message: T) -> Result<(), TrySendError<T>> {
94            let res = self.0.try_send(message);
95            if res.is_ok() {
96                super::detail::trace_pulse::<T>("mpsc", std::panic::Location::caller());
97            }
98            res
99        }
100    }
101
102    impl<T: crate::sync::TypeChannelName> Deref for Sender<T> {
103        type Target = tokio::sync::mpsc::Sender<T>;
104        fn deref(&self) -> &Self::Target {
105            &self.0
106        }
107    }
108
109    impl<T: crate::sync::TypeChannelName> From<tokio::sync::mpsc::Sender<T>> for Sender<T> {
110        fn from(inner: tokio::sync::mpsc::Sender<T>) -> Self {
111            Self(inner)
112        }
113    }
114
115    impl<T: crate::sync::TypeChannelName> From<Sender<T>> for tokio::sync::mpsc::Sender<T> {
116        fn from(proxy: Sender<T>) -> Self {
117            proxy.0
118        }
119    }
120
121    /// An instrumented wrapper around `tokio::sync::mpsc::UnboundedSender`.
122    #[derive(Debug)]
123    #[repr(transparent)]
124    pub struct UnboundedSender<T: crate::sync::TypeChannelName>(
125        tokio::sync::mpsc::UnboundedSender<T>,
126    );
127
128    impl<T: crate::sync::TypeChannelName> Clone for UnboundedSender<T> {
129        fn clone(&self) -> Self {
130            Self(self.0.clone())
131        }
132    }
133
134    impl<T: crate::sync::TypeChannelName> UnboundedSender<T> {
135        /// Sends a message to the corresponding `UnboundedReceiver`.
136        ///
137        /// Automatically emits a telemetry pulse upon successful send.
138        #[track_caller]
139        pub fn send(&self, message: T) -> Result<(), error::SendError<T>> {
140            let res = self.0.send(message);
141            if res.is_ok() {
142                super::detail::trace_pulse::<T>("unbounded", std::panic::Location::caller());
143            }
144            res
145        }
146    }
147
148    impl<T: crate::sync::TypeChannelName> Deref for UnboundedSender<T> {
149        type Target = tokio::sync::mpsc::UnboundedSender<T>;
150        fn deref(&self) -> &Self::Target {
151            &self.0
152        }
153    }
154
155    impl<T: crate::sync::TypeChannelName> From<tokio::sync::mpsc::UnboundedSender<T>>
156        for UnboundedSender<T>
157    {
158        fn from(inner: tokio::sync::mpsc::UnboundedSender<T>) -> Self {
159            Self(inner)
160        }
161    }
162
163    impl<T: crate::sync::TypeChannelName> From<UnboundedSender<T>>
164        for tokio::sync::mpsc::UnboundedSender<T>
165    {
166        fn from(proxy: UnboundedSender<T>) -> Self {
167            proxy.0
168        }
169    }
170}
171
172#[cfg(debug_assertions)]
173pub mod broadcast {
174    //! Instrumented `broadcast` channels.
175    use std::ops::Deref;
176    use tokio::sync::broadcast::error::SendError;
177    pub use tokio::sync::broadcast::{Receiver, error};
178
179    /// Creates a multi-producer, multi-consumer broadcast channel.
180    ///
181    /// The returned `Sender` is instrumented with automatic telemetry pulses.
182    /// The message type `T` must be registered via `register_channel_name!`.
183    #[track_caller]
184    pub fn channel<T: Clone + crate::sync::TypeChannelName>(
185        capacity: usize,
186    ) -> (Sender<T>, Receiver<T>) {
187        let (tx, rx) = tokio::sync::broadcast::channel(capacity);
188        (Sender(tx), rx)
189    }
190
191    /// An instrumented wrapper around `tokio::sync::broadcast::Sender`.
192    #[derive(Debug)]
193    #[repr(transparent)]
194    pub struct Sender<T: Clone + crate::sync::TypeChannelName>(tokio::sync::broadcast::Sender<T>);
195
196    impl<T: Clone + crate::sync::TypeChannelName> Clone for Sender<T> {
197        fn clone(&self) -> Self {
198            Self(self.0.clone())
199        }
200    }
201
202    impl<T: Clone + crate::sync::TypeChannelName> Sender<T> {
203        /// Sends a message to all active receivers.
204        ///
205        /// Automatically emits a telemetry pulse upon successful send.
206        #[track_caller]
207        pub fn send(&self, value: T) -> Result<usize, SendError<T>> {
208            let res = self.0.send(value);
209            if res.is_ok() {
210                super::detail::trace_pulse::<T>("broadcast", std::panic::Location::caller());
211            }
212            res
213        }
214
215        /// Subscribes to this broadcast channel.
216        pub fn subscribe(&self) -> Receiver<T> {
217            self.0.subscribe()
218        }
219    }
220
221    impl<T: Clone + crate::sync::TypeChannelName> Deref for Sender<T> {
222        type Target = tokio::sync::broadcast::Sender<T>;
223        fn deref(&self) -> &Self::Target {
224            &self.0
225        }
226    }
227
228    impl<T: Clone + crate::sync::TypeChannelName> From<tokio::sync::broadcast::Sender<T>>
229        for Sender<T>
230    {
231        fn from(inner: tokio::sync::broadcast::Sender<T>) -> Self {
232            Self(inner)
233        }
234    }
235
236    impl<T: Clone + crate::sync::TypeChannelName> From<Sender<T>>
237        for tokio::sync::broadcast::Sender<T>
238    {
239        fn from(proxy: Sender<T>) -> Self {
240            proxy.0
241        }
242    }
243}
244
245#[cfg(debug_assertions)]
246pub use tokio::sync::{
247    Barrier, Mutex, Notify, OnceCell, OwnedRwLockReadGuard, OwnedRwLockWriteGuard,
248    OwnedSemaphorePermit, RwLock, RwLockReadGuard, RwLockWriteGuard, Semaphore, SemaphorePermit,
249    TryLockError, futures, oneshot, watch,
250};
251
252/// Trait used to provide a semantic name for a type in telemetry pulses.
253///
254/// This is used by the `register_channel_name!` macro to associate a friendly
255/// string with a specific message type.
256pub trait TypeChannelName {
257    /// The semantic name of the channel/message type.
258    const CHANNEL_NAME: &'static str;
259}
260
261/// Registers a semantic name for a message type to be used in telemetry pulses.
262///
263/// ## Example
264///
265/// ```rust
266/// use synapto_interface::register_channel_name;
267/// pub struct MyMessage;
268/// register_channel_name!(MyMessage, "my_custom_channel");
269/// ```
270///
271/// This will result in Rerun metrics starting with `my_custom_channel:`.
272#[macro_export]
273macro_rules! register_channel_name {
274    ($type_name:ty, $channel_name:expr) => {
275        #[cfg(debug_assertions)]
276        impl $crate::sync::TypeChannelName for $type_name {
277            const CHANNEL_NAME: &'static str = $channel_name;
278        }
279    };
280}
281
282#[cfg(debug_assertions)]
283mod detail {
284    use std::panic::Location;
285
286    #[inline(always)]
287    pub(super) fn trace_pulse<T: crate::sync::TypeChannelName>(
288        channel_type: &'static str,
289        location: &Location<'_>,
290    ) {
291        let type_name = std::any::type_name::<T>();
292        let short_type = type_name
293            .rsplit_once("::")
294            .map(|(_, s)| s)
295            .unwrap_or(type_name);
296
297        let file = location.file();
298        let filepath = if let Some((_, post_src)) = file.rsplit_once("src/") {
299            post_src
300                .strip_suffix(".rs")
301                .unwrap_or(post_src)
302                .strip_suffix("/mod")
303                .unwrap_or(post_src)
304                .replace(['/', '\\'], "::")
305        } else {
306            file.rsplit_once('/')
307                .or_else(|| file.rsplit_once('\\'))
308                .map(|(_, s)| s)
309                .unwrap_or(file)
310                .to_string()
311        };
312
313        let metric_name = format!(
314            "{}:{}:{}@{}",
315            T::CHANNEL_NAME,
316            channel_type,
317            short_type,
318            filepath
319        );
320        tracing::trace!(target: "telemetry", metric = %metric_name, value = 1.0);
321    }
322}
323
324#[cfg(test)]
325#[allow(clippy::disallowed_methods)]
326mod tests {
327    use super::*;
328
329    struct SemanticType;
330    register_channel_name!(SemanticType, "semantic_channel");
331
332    register_channel_name!(i32, "test_i32");
333
334    #[tokio::test]
335    async fn test_mpsc_proxy() {
336        let (tx, mut rx) = mpsc::channel::<i32>(1);
337        tx.send(42).await.unwrap();
338        assert_eq!(rx.recv().await.unwrap(), 42);
339
340        tx.try_send(43).unwrap();
341        assert_eq!(rx.recv().await.unwrap(), 43);
342
343        // Test Deref
344        assert_eq!(tx.capacity(), 1);
345
346        // Test interop
347        let tokio_tx: tokio::sync::mpsc::Sender<i32> = tx.into();
348        tokio_tx.send(44).await.unwrap();
349        assert_eq!(rx.recv().await.unwrap(), 44);
350    }
351
352    #[tokio::test]
353    async fn test_unbounded_mpsc_proxy() {
354        let (tx, mut rx) = mpsc::unbounded_channel::<i32>();
355        tx.send(42).unwrap();
356        assert_eq!(rx.recv().await.unwrap(), 42);
357
358        // Test Deref
359        assert!(!tx.is_closed());
360
361        // Test interop
362        let tokio_tx: tokio::sync::mpsc::UnboundedSender<i32> = tx.into();
363        tokio_tx.send(44).unwrap();
364        assert_eq!(rx.recv().await.unwrap(), 44);
365    }
366
367    #[tokio::test]
368    async fn test_broadcast_proxy() {
369        let (tx, mut rx1) = broadcast::channel::<i32>(10);
370        let mut rx2 = tx.subscribe();
371
372        tx.send(42).unwrap();
373        assert_eq!(rx1.recv().await.unwrap(), 42);
374        assert_eq!(rx2.recv().await.unwrap(), 42);
375
376        // Test Deref
377        assert_eq!(tx.receiver_count(), 2);
378
379        // Test interop
380        let tokio_tx: tokio::sync::broadcast::Sender<i32> = tx.into();
381        tokio_tx.send(44).unwrap();
382        assert_eq!(rx1.recv().await.unwrap(), 44);
383        assert_eq!(rx2.recv().await.unwrap(), 44);
384    }
385
386    #[tokio::test]
387    async fn test_semantic_naming() {
388        let (tx, _rx) = mpsc::channel::<SemanticType>(1);
389        tx.send(SemanticType).await.unwrap();
390        // This test primarily verifies compilation and the tagging pattern works.
391        // The actual tracing output is not easily verified in unit tests without a subscriber.
392    }
393}