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}