Skip to main content

dynomite/io/
reactor.rs

1//! Async transport abstraction over the connection state machine.
2//!
3//! The reactor is built on tokio. This module defines a
4//! [`Transport`] trait that downstream stages drive through
5//! `tokio::io::{AsyncRead, AsyncWrite}` and a [`ConnRole`] enum that
6//! enumerates the connection roles (`client`, `server`, `proxy`,
7//! plus their dnode peer variants).
8//!
9//! The trait is intentionally narrow:
10//!
11//! * [`Transport::role`] returns the connection role for routing /
12//!   metric tagging.
13//! * [`Transport::peer_addr`] returns the remote address when the
14//!   transport is connected to a known endpoint. QUIC, for example,
15//!   may not always have one.
16//!
17//! `Transport` does not expose any TCP-specific operations; this is
18//! deliberate so the QUIC implementation can wrap a
19//! `quiche::Connection` in a `QuicTransport` newtype without changing
20//! callers.
21//!
22//! [`TcpTransport`] is the TCP implementation, a newtype wrapper
23//! around [`tokio::net::TcpStream`].
24//!
25//! # Examples
26//!
27//! ```
28//! use dynomite::io::reactor::{ConnRole, TcpTransport, Transport};
29//! use tokio::io::{AsyncReadExt, AsyncWriteExt};
30//! use tokio::net::{TcpListener, TcpStream};
31//!
32//! # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
33//! let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
34//! let addr = listener.local_addr().unwrap();
35//! let server = tokio::spawn(async move {
36//!     let (sock, _) = listener.accept().await.unwrap();
37//!     let mut t = TcpTransport::new(sock, ConnRole::Server);
38//!     let mut buf = [0u8; 5];
39//!     t.read_exact(&mut buf).await.unwrap();
40//!     t.write_all(&buf).await.unwrap();
41//! });
42//! let sock = TcpStream::connect(addr).await.unwrap();
43//! let mut client = TcpTransport::new(sock, ConnRole::Client);
44//! client.write_all(b"hello").await.unwrap();
45//! let mut out = [0u8; 5];
46//! client.read_exact(&mut out).await.unwrap();
47//! assert_eq!(&out, b"hello");
48//! server.await.unwrap();
49//! # });
50//! ```
51
52use std::io;
53use std::net::SocketAddr;
54use std::pin::Pin;
55use std::task::{Context, Poll};
56
57use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
58use tokio::net::TcpStream;
59
60/// Role tag for a connection.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub enum ConnRole {
63    /// Client-facing listener that accepted the connection.
64    Proxy,
65    /// Connection from a client driver (Redis or Memcached).
66    Client,
67    /// Connection to a backend datastore (Redis or Memcached).
68    Server,
69    /// Listener that accepts dnode peer connections from other nodes.
70    DnodePeerProxy,
71    /// Inbound dnode peer connection (a remote node connected to us).
72    DnodePeerClient,
73    /// Outbound dnode peer connection (we connected to a remote node).
74    DnodePeerServer,
75}
76
77impl ConnRole {
78    /// True when the role represents a listening socket.
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use dynomite::io::reactor::ConnRole;
84    /// assert!(ConnRole::Proxy.is_listener());
85    /// assert!(!ConnRole::Client.is_listener());
86    /// ```
87    pub fn is_listener(self) -> bool {
88        matches!(self, Self::Proxy | Self::DnodePeerProxy)
89    }
90
91    /// True when the role represents a peer-to-peer dnode link.
92    ///
93    /// # Examples
94    ///
95    /// ```
96    /// use dynomite::io::reactor::ConnRole;
97    /// assert!(ConnRole::DnodePeerClient.is_dnode_peer());
98    /// assert!(!ConnRole::Client.is_dnode_peer());
99    /// ```
100    pub fn is_dnode_peer(self) -> bool {
101        matches!(
102            self,
103            Self::DnodePeerProxy | Self::DnodePeerClient | Self::DnodePeerServer
104        )
105    }
106}
107
108/// Generic async byte-stream the engine reads and writes through.
109///
110/// Implementors must be [`Send`] and [`Unpin`] so they fit the tokio
111/// task model. Adding a new transport (QUIC, for example)
112/// means newtyping its connection handle and implementing
113/// [`AsyncRead`], [`AsyncWrite`], and `Transport`.
114pub trait Transport: AsyncRead + AsyncWrite + Send + Unpin {
115    /// Connection role.
116    fn role(&self) -> ConnRole;
117
118    /// Remote socket address when the transport is connected to one.
119    /// Returns `None` for transports that do not surface a
120    /// `SocketAddr` (for example, QUIC over a virtual interface).
121    fn peer_addr(&self) -> Option<SocketAddr>;
122}
123
124/// TCP-backed [`Transport`].
125///
126/// Newtype around [`tokio::net::TcpStream`]. The role is supplied at
127/// construction time because the same TCP socket type is used for
128/// every C role variant; the discriminator lives one level up in the
129/// listener registration.
130#[derive(Debug)]
131pub struct TcpTransport {
132    inner: TcpStream,
133    role: ConnRole,
134}
135
136impl TcpTransport {
137    /// Wrap a TCP stream with the given role tag.
138    ///
139    /// # Examples
140    ///
141    /// ```
142    /// use dynomite::io::reactor::{ConnRole, TcpTransport, Transport};
143    /// use tokio::net::{TcpListener, TcpStream};
144    ///
145    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
146    /// let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
147    /// let addr = listener.local_addr().unwrap();
148    /// let _accept = tokio::spawn(async move {
149    ///     let (s, _) = listener.accept().await.unwrap();
150    ///     drop(s);
151    /// });
152    /// let sock = TcpStream::connect(addr).await.unwrap();
153    /// let t = TcpTransport::new(sock, ConnRole::Client);
154    /// assert_eq!(t.role(), ConnRole::Client);
155    /// # });
156    /// ```
157    pub fn new(stream: TcpStream, role: ConnRole) -> Self {
158        Self {
159            inner: stream,
160            role,
161        }
162    }
163
164    /// Borrow the wrapped tokio stream.
165    pub fn get_ref(&self) -> &TcpStream {
166        &self.inner
167    }
168
169    /// Mutably borrow the wrapped tokio stream. Useful for setting
170    /// per-socket options (`set_nodelay`, `set_linger`, ...) without
171    /// re-implementing them on the wrapper.
172    pub fn get_mut(&mut self) -> &mut TcpStream {
173        &mut self.inner
174    }
175
176    /// Consume the wrapper and return the inner stream.
177    pub fn into_inner(self) -> TcpStream {
178        self.inner
179    }
180}
181
182impl Transport for TcpTransport {
183    fn role(&self) -> ConnRole {
184        self.role
185    }
186
187    fn peer_addr(&self) -> Option<SocketAddr> {
188        self.inner.peer_addr().ok()
189    }
190}
191
192impl AsyncRead for TcpTransport {
193    fn poll_read(
194        mut self: Pin<&mut Self>,
195        cx: &mut Context<'_>,
196        buf: &mut ReadBuf<'_>,
197    ) -> Poll<io::Result<()>> {
198        Pin::new(&mut self.inner).poll_read(cx, buf)
199    }
200}
201
202impl AsyncWrite for TcpTransport {
203    fn poll_write(
204        mut self: Pin<&mut Self>,
205        cx: &mut Context<'_>,
206        buf: &[u8],
207    ) -> Poll<io::Result<usize>> {
208        Pin::new(&mut self.inner).poll_write(cx, buf)
209    }
210
211    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
212        Pin::new(&mut self.inner).poll_flush(cx)
213    }
214
215    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
216        Pin::new(&mut self.inner).poll_shutdown(cx)
217    }
218
219    fn poll_write_vectored(
220        mut self: Pin<&mut Self>,
221        cx: &mut Context<'_>,
222        bufs: &[io::IoSlice<'_>],
223    ) -> Poll<io::Result<usize>> {
224        Pin::new(&mut self.inner).poll_write_vectored(cx, bufs)
225    }
226
227    fn is_write_vectored(&self) -> bool {
228        self.inner.is_write_vectored()
229    }
230}
231
232/// Unix-domain-socket-backed [`Transport`].
233///
234/// Newtype around [`tokio::net::UnixStream`], used when a datastore
235/// backend is configured with a filesystem path rather than a
236/// `host:port` endpoint. Unix sockets do not expose a
237/// [`SocketAddr`], so [`Transport::peer_addr`] returns `None`.
238#[cfg(unix)]
239#[derive(Debug)]
240pub struct UnixTransport {
241    inner: tokio::net::UnixStream,
242    role: ConnRole,
243}
244
245#[cfg(unix)]
246impl UnixTransport {
247    /// Wrap a Unix-domain stream with the given role tag.
248    ///
249    /// # Examples
250    ///
251    /// ```
252    /// use dynomite::io::reactor::{ConnRole, Transport, UnixTransport};
253    /// use tokio::net::{UnixListener, UnixStream};
254    ///
255    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
256    /// let dir = std::env::temp_dir().join(format!("dyn-ut-{}", std::process::id()));
257    /// let _ = std::fs::remove_file(&dir);
258    /// let listener = UnixListener::bind(&dir).unwrap();
259    /// let _accept = tokio::spawn(async move {
260    ///     let (s, _) = listener.accept().await.unwrap();
261    ///     drop(s);
262    /// });
263    /// let sock = UnixStream::connect(&dir).await.unwrap();
264    /// let t = UnixTransport::new(sock, ConnRole::Server);
265    /// assert_eq!(t.role(), ConnRole::Server);
266    /// assert!(t.peer_addr().is_none());
267    /// let _ = std::fs::remove_file(&dir);
268    /// # });
269    /// ```
270    pub fn new(stream: tokio::net::UnixStream, role: ConnRole) -> Self {
271        Self {
272            inner: stream,
273            role,
274        }
275    }
276
277    /// Consume the wrapper and return the inner stream.
278    pub fn into_inner(self) -> tokio::net::UnixStream {
279        self.inner
280    }
281}
282
283#[cfg(unix)]
284impl Transport for UnixTransport {
285    fn role(&self) -> ConnRole {
286        self.role
287    }
288
289    fn peer_addr(&self) -> Option<SocketAddr> {
290        None
291    }
292}
293
294#[cfg(unix)]
295impl AsyncRead for UnixTransport {
296    fn poll_read(
297        mut self: Pin<&mut Self>,
298        cx: &mut Context<'_>,
299        buf: &mut ReadBuf<'_>,
300    ) -> Poll<io::Result<()>> {
301        Pin::new(&mut self.inner).poll_read(cx, buf)
302    }
303}
304
305#[cfg(unix)]
306impl AsyncWrite for UnixTransport {
307    fn poll_write(
308        mut self: Pin<&mut Self>,
309        cx: &mut Context<'_>,
310        buf: &[u8],
311    ) -> Poll<io::Result<usize>> {
312        Pin::new(&mut self.inner).poll_write(cx, buf)
313    }
314
315    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
316        Pin::new(&mut self.inner).poll_flush(cx)
317    }
318
319    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
320        Pin::new(&mut self.inner).poll_shutdown(cx)
321    }
322
323    fn poll_write_vectored(
324        mut self: Pin<&mut Self>,
325        cx: &mut Context<'_>,
326        bufs: &[io::IoSlice<'_>],
327    ) -> Poll<io::Result<usize>> {
328        Pin::new(&mut self.inner).poll_write_vectored(cx, bufs)
329    }
330
331    fn is_write_vectored(&self) -> bool {
332        self.inner.is_write_vectored()
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339    use tokio::io::{AsyncReadExt, AsyncWriteExt};
340    use tokio::net::{TcpListener, TcpStream};
341
342    #[test]
343    fn role_predicates() {
344        assert!(ConnRole::Proxy.is_listener());
345        assert!(ConnRole::DnodePeerProxy.is_listener());
346        assert!(!ConnRole::Server.is_listener());
347        assert!(ConnRole::DnodePeerServer.is_dnode_peer());
348        assert!(!ConnRole::Server.is_dnode_peer());
349    }
350
351    #[tokio::test]
352    async fn tcp_transport_round_trip() {
353        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
354        let addr = listener.local_addr().unwrap();
355        let server = tokio::spawn(async move {
356            let (s, _) = listener.accept().await.unwrap();
357            let mut t = TcpTransport::new(s, ConnRole::Server);
358            assert_eq!(t.role(), ConnRole::Server);
359            let mut buf = [0u8; 4];
360            t.read_exact(&mut buf).await.unwrap();
361            t.write_all(&buf).await.unwrap();
362        });
363        let sock = TcpStream::connect(addr).await.unwrap();
364        let mut client = TcpTransport::new(sock, ConnRole::Client);
365        assert_eq!(client.role(), ConnRole::Client);
366        assert!(client.peer_addr().is_some());
367        client.write_all(b"ping").await.unwrap();
368        let mut out = [0u8; 4];
369        client.read_exact(&mut out).await.unwrap();
370        assert_eq!(&out, b"ping");
371        server.await.unwrap();
372    }
373}