Skip to main content

mcrx_core/
tokio_adapter.rs

1use crate::{McrxError, Packet, PacketWithMetadata, Subscription};
2use std::io;
3#[cfg(not(unix))]
4use std::time::Duration;
5use thiserror::Error;
6
7/// Errors returned by the Tokio adapter.
8#[derive(Debug, Error)]
9pub enum TokioReceiveError {
10    /// Waiting for Tokio readiness failed.
11    #[error("MCRX: tokio readiness failed: {0}")]
12    Readiness(io::Error),
13
14    /// The underlying multicast receiver returned an error.
15    #[error(transparent)]
16    Receive(#[from] McrxError),
17}
18
19/// Thin Tokio wrapper around an owned subscription.
20///
21/// On Unix this uses `tokio::io::unix::AsyncFd` to wait for read readiness.
22/// On other platforms it falls back to an async sleep-and-poll loop.
23///
24/// The async receive methods take `&mut self` because the adapter is designed
25/// for a single receiving task at a time. This also keeps the Tokio receive
26/// future `Send` when the optional `metrics` feature is enabled.
27#[derive(Debug)]
28pub struct TokioSubscription {
29    #[cfg(unix)]
30    inner: tokio::io::unix::AsyncFd<Subscription>,
31    #[cfg(not(unix))]
32    inner: Subscription,
33    #[cfg(not(unix))]
34    poll_interval: Duration,
35}
36
37impl TokioSubscription {
38    /// Wraps an owned subscription for use with Tokio.
39    pub fn new(subscription: Subscription) -> io::Result<Self> {
40        #[cfg(unix)]
41        {
42            Ok(Self {
43                inner: tokio::io::unix::AsyncFd::new(subscription)?,
44            })
45        }
46
47        #[cfg(not(unix))]
48        {
49            Ok(Self {
50                inner: subscription,
51                poll_interval: Duration::from_millis(10),
52            })
53        }
54    }
55
56    /// Returns a shared reference to the wrapped subscription.
57    pub fn subscription(&self) -> &Subscription {
58        #[cfg(unix)]
59        {
60            self.inner.get_ref()
61        }
62
63        #[cfg(not(unix))]
64        {
65            &self.inner
66        }
67    }
68
69    /// Consumes the adapter and returns the wrapped subscription.
70    pub fn into_subscription(self) -> Subscription {
71        #[cfg(unix)]
72        {
73            self.inner.into_inner()
74        }
75
76        #[cfg(not(unix))]
77        {
78            self.inner
79        }
80    }
81
82    /// Overrides the async poll interval used on platforms without `AsyncFd`.
83    #[cfg(not(unix))]
84    pub fn with_poll_interval(mut self, poll_interval: Duration) -> Self {
85        self.poll_interval = poll_interval;
86        self
87    }
88
89    /// Waits for the next packet and returns it.
90    pub async fn recv(&mut self) -> Result<Packet, TokioReceiveError> {
91        #[cfg(unix)]
92        {
93            loop {
94                let mut readiness = self
95                    .inner
96                    .readable_mut()
97                    .await
98                    .map_err(TokioReceiveError::Readiness)?;
99
100                match readiness.get_inner_mut().try_recv()? {
101                    Some(packet) => return Ok(packet),
102                    None => readiness.clear_ready(),
103                }
104            }
105        }
106
107        #[cfg(not(unix))]
108        {
109            loop {
110                match self.inner.try_recv()? {
111                    Some(packet) => return Ok(packet),
112                    None => tokio::time::sleep(self.poll_interval).await,
113                }
114            }
115        }
116    }
117
118    /// Waits for the next packet with richer receive metadata and returns it.
119    pub async fn recv_with_metadata(&mut self) -> Result<PacketWithMetadata, TokioReceiveError> {
120        #[cfg(unix)]
121        {
122            loop {
123                let mut readiness = self
124                    .inner
125                    .readable_mut()
126                    .await
127                    .map_err(TokioReceiveError::Readiness)?;
128
129                match readiness.get_inner_mut().try_recv_with_metadata()? {
130                    Some(packet) => return Ok(packet),
131                    None => readiness.clear_ready(),
132                }
133            }
134        }
135
136        #[cfg(not(unix))]
137        {
138            loop {
139                match self.inner.try_recv_with_metadata()? {
140                    Some(packet) => return Ok(packet),
141                    None => tokio::time::sleep(self.poll_interval).await,
142                }
143            }
144        }
145    }
146}
147
148#[cfg(all(test, feature = "tokio"))]
149mod tests {
150    use super::*;
151    use crate::test_support::{make_multicast_sender, sample_config_on_unused_port};
152    use crate::{Context, SubscriptionConfig};
153    use std::net::{Ipv4Addr, SocketAddrV4};
154    use tokio::time::{Duration, timeout};
155
156    fn ipv4_group(config: &SubscriptionConfig) -> Ipv4Addr {
157        config.ipv4_membership().unwrap().group
158    }
159
160    #[tokio::test]
161    async fn tokio_subscription_receives_metadata_packet() {
162        let mut context = Context::new();
163        let config = sample_config_on_unused_port();
164        let id = context.add_subscription(config.clone()).unwrap();
165        context.join_subscription(id).unwrap();
166
167        let subscription = context.take_subscription(id).unwrap();
168        let mut subscription = TokioSubscription::new(subscription).unwrap();
169
170        let sender = make_multicast_sender();
171        let payload = b"tokio adapter packet";
172        sender
173            .send_to(
174                payload,
175                SocketAddrV4::new(ipv4_group(&config), config.dst_port),
176            )
177            .unwrap();
178
179        let packet = timeout(Duration::from_secs(1), subscription.recv_with_metadata())
180            .await
181            .expect("timed out waiting for tokio packet")
182            .unwrap();
183
184        assert_eq!(packet.packet.subscription_id, id);
185        assert_eq!(&packet.packet.payload[..], payload);
186    }
187
188    #[cfg(feature = "metrics")]
189    #[tokio::test]
190    async fn tokio_subscription_with_metrics_is_spawn_safe() {
191        let mut context = Context::new();
192        let config = sample_config_on_unused_port();
193        let id = context.add_subscription(config.clone()).unwrap();
194        context.join_subscription(id).unwrap();
195
196        let subscription = context.take_subscription(id).unwrap();
197        let sender = make_multicast_sender();
198        let payload = b"tokio metrics packet";
199
200        let handle = tokio::spawn(async move {
201            let mut subscription = TokioSubscription::new(subscription).unwrap();
202            let packet = timeout(Duration::from_secs(1), subscription.recv_with_metadata())
203                .await
204                .expect("timed out waiting for spawned tokio packet")
205                .unwrap();
206            let metrics = subscription.subscription().metrics_snapshot();
207            (packet, metrics)
208        });
209
210        sender
211            .send_to(
212                payload,
213                SocketAddrV4::new(ipv4_group(&config), config.dst_port),
214            )
215            .unwrap();
216
217        let (packet, metrics) = handle.await.unwrap();
218
219        assert_eq!(packet.packet.subscription_id, id);
220        assert_eq!(&packet.packet.payload[..], payload);
221        assert_eq!(metrics.packets_received, 1);
222        assert_eq!(metrics.bytes_received, payload.len() as u64);
223        assert_eq!(metrics.last_payload_len, Some(payload.len()));
224    }
225}