Skip to main content

dynomite/net/
dnode_server.rs

1//! DNODE_PEER_SERVER-role connection driver.
2//!
3//! Outbound peer connection: the local node initiates the link to
4//! a remote peer. The driver wraps every outgoing request in a
5//! dnode header (and, when the pool's `secure_server_option`
6//! requires it, encrypts the payload), pumps the wire bytes, then
7//! parses the response that comes back and delivers it through
8//! the per-request responder channel.
9
10use tokio::io::{AsyncReadExt, AsyncWriteExt};
11use tokio::sync::mpsc;
12use tracing::Instrument as _;
13
14use crate::core::types::MsgId;
15use crate::io::reactor::ConnRole;
16use crate::msg::{Msg, MsgParseResult, MsgType};
17use crate::net::conn::Conn;
18use crate::net::dispatcher::OutboundEnvelope;
19use crate::net::server::OutboundRequest;
20use crate::net::NetError;
21use crate::proto::dnode::{dmsg_write, DmsgType, DnodeParser, ParseStep};
22
23/// True when the dnode message type expects a matching response
24/// frame from the peer (data plane). Gossip variants are
25/// fire-and-forget and never push onto the per-connection
26/// pending queue.
27fn is_data_plane_ty(ty: DmsgType) -> bool {
28    matches!(
29        ty,
30        DmsgType::Req | DmsgType::ReqForward | DmsgType::Res | DmsgType::Unknown
31    )
32}
33
34/// Outbound DNODE peer connection driver.
35pub struct DnodeServerConn {
36    conn: Conn,
37    requests: mpsc::Receiver<OutboundRequest>,
38    pending: std::collections::VecDeque<(
39        MsgId,
40        tracing::Span,
41        Option<u32>,
42        mpsc::Sender<OutboundEnvelope>,
43    )>,
44}
45
46impl DnodeServerConn {
47    /// Wrap an outbound peer connection.
48    ///
49    /// # Examples
50    ///
51    /// ```no_run
52    /// use dynomite::io::reactor::{ConnRole, TcpTransport};
53    /// use dynomite::net::{Conn, DnodeServerConn};
54    /// use tokio::sync::mpsc;
55    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
56    /// let s = tokio::net::TcpStream::connect("127.0.0.1:0").await.unwrap();
57    /// let conn = Conn::new(Box::new(TcpTransport::new(s, ConnRole::DnodePeerServer)), ConnRole::DnodePeerServer);
58    /// let (_tx, rx) = mpsc::channel(8);
59    /// let _ = DnodeServerConn::new(conn, rx);
60    /// # });
61    /// ```
62    #[must_use]
63    pub fn new(conn: Conn, requests: mpsc::Receiver<OutboundRequest>) -> Self {
64        debug_assert!(matches!(conn.role(), ConnRole::DnodePeerServer));
65        Self {
66            conn,
67            requests,
68            pending: std::collections::VecDeque::new(),
69        }
70    }
71
72    /// Drive the FSM.
73    ///
74    /// # Errors
75    /// Forwarded transport / DNODE parse errors.
76    pub async fn run(mut self) -> Result<(), NetError> {
77        let mut requests = std::mem::replace(&mut self.requests, {
78            let (_tx, rx) = mpsc::channel::<OutboundRequest>(1);
79            rx
80        });
81        self.run_with(&mut requests).await
82    }
83
84    /// Drive the FSM using a borrowed request receiver. Useful
85    /// for reconnect supervisors that own the receiver across
86    /// multiple connection attempts and pass it in by reference.
87    ///
88    /// # Errors
89    /// Forwarded transport / DNODE parse errors.
90    pub async fn run_with(
91        &mut self,
92        requests: &mut mpsc::Receiver<OutboundRequest>,
93    ) -> Result<(), NetError> {
94        let mut read_buf = vec![0u8; 4096];
95        let mut accumulated = Vec::<u8>::new();
96        let mut parser = DnodeParser::new();
97
98        loop {
99            if self.conn.is_eof() && self.pending.is_empty() {
100                self.conn.set_done();
101                return Ok(());
102            }
103
104            tokio::select! {
105                req = requests.recv() => {
106                    let Some(req) = req else { continue; };
107                    let send_span = tracing::info_span!(
108                        parent: &req.span,
109                        "peer.send",
110                        req_id = req.req_id,
111                        bytes = req.bytes.len(),
112                    );
113                    let req_span = req.span.clone();
114                    let req_bytes = req.bytes;
115                    let req_id = req.req_id;
116                    let req_ty = req.ty;
117                    let mut header_buf = self.conn.mbuf_pool().get();
118                    dmsg_write(
119                        &mut header_buf,
120                        req_id,
121                        if matches!(req_ty, DmsgType::Unknown) { DmsgType::Req } else { req_ty },
122                        0,
123                        true,
124                        None,
125                        u32::try_from(req_bytes.len()).unwrap_or(u32::MAX),
126                    )?;
127                    let header_len = header_buf.readable().len();
128                    let transport = self.conn.transport_mut().ok_or(NetError::Closed)?;
129                    let write_res = async {
130                        transport.write_all(header_buf.readable()).await?;
131                        transport.write_all(&req_bytes).await?;
132                        Ok::<(), std::io::Error>(())
133                    }
134                    .instrument(send_span)
135                    .await;
136                    write_res?;
137                    self.conn.record_send(header_len + req_bytes.len());
138                    if is_data_plane_ty(req_ty) {
139                        // Pair the responder WITH its req_id in the
140                        // pending queue. A single shared responder slot
141                        // would deliver a reply to whichever request
142                        // was enqueued LAST, so under fan-out (several
143                        // concurrent requests multiplexed on one peer
144                        // connection) replies cross wires. FIFO order
145                        // matches the dnode reply order.
146                        self.pending.push_back((
147                            req_id,
148                            req_span,
149                            req.target_peer_idx,
150                            req.responder,
151                        ));
152                    } else {
153                        // Gossip / control-plane frames are
154                        // fire-and-forget; the responder is
155                        // dropped here and the originator does
156                        // not block waiting for an ACK.
157                        drop(req.responder);
158                    }
159                }
160                read_res = async {
161                    if let Some(t) = self.conn.transport_mut() {
162                        t.read(&mut read_buf).await
163                    } else {
164                        Ok(0)
165                    }
166                } => {
167                    let n = read_res?;
168                    if n == 0 {
169                        self.conn.set_eof();
170                        continue;
171                    }
172                    self.conn.record_recv(n);
173                    accumulated.extend_from_slice(&read_buf[..n]);
174                    self.drive_response(&mut accumulated, &mut parser).await?;
175                }
176            }
177        }
178    }
179
180    async fn drive_response(
181        &mut self,
182        accumulated: &mut Vec<u8>,
183        parser: &mut DnodeParser,
184    ) -> Result<(), NetError> {
185        loop {
186            if accumulated.is_empty() {
187                return Ok(());
188            }
189            let step = parser.step(accumulated.as_slice());
190            match step {
191                ParseStep::NeedMore { .. } => return Ok(()),
192                ParseStep::Error { consumed } => {
193                    return Err(NetError::Dnode(format!(
194                        "dnode peer-server parse error after {consumed} bytes"
195                    )));
196                }
197                ParseStep::HeaderDone { consumed } => {
198                    let dmsg = parser.take_dmsg();
199                    let plen = dmsg.plen as usize;
200                    let total = consumed + plen;
201                    if accumulated.len() < total {
202                        parser.reset();
203                        return Ok(());
204                    }
205                    let payload = accumulated[consumed..total].to_vec();
206                    accumulated.drain(0..total);
207                    parser.reset();
208
209                    // Build the response Msg from the payload bytes.
210                    // Pop the pending entry whose req_id this reply
211                    // answers (dnode replies arrive in request order),
212                    // recovering its paired responder so the reply
213                    // goes back to the request that issued it.
214                    let (req_id, req_span, source_peer_idx, reply_to) =
215                        match self.pending.pop_front() {
216                            Some(entry) => (entry.0, entry.1, entry.2, Some(entry.3)),
217                            None => (dmsg.id, tracing::Span::current(), None, None),
218                        };
219                    let parse_span = tracing::info_span!(
220                        parent: &req_span,
221                        "peer.parse",
222                        req_id,
223                        bytes = plen,
224                    );
225                    let env = parse_span.in_scope(|| {
226                        let mut rsp = Msg::new(req_id, MsgType::Unknown, false);
227                        let pool = self.conn.mbuf_pool().clone();
228                        let mut buf = pool.get();
229                        buf.recv(&payload);
230                        rsp.mbufs_mut().push_back(buf);
231                        rsp.recompute_mlen();
232                        rsp.set_dmsg(dmsg);
233                        // Mark parse outcome so consumers can branch
234                        // on a successful peer round-trip.
235                        rsp.set_parse_result(MsgParseResult::Ok);
236                        OutboundEnvelope {
237                            req_id,
238                            rsp,
239                            span: req_span,
240                            source_peer_idx,
241                        }
242                    });
243                    if let Some(sender) = reply_to.as_ref() {
244                        let _ = sender.send(env).await;
245                    }
246                }
247            }
248        }
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use crate::io::reactor::TcpTransport;
256    use tokio::net::{TcpListener, TcpStream};
257
258    #[tokio::test]
259    async fn build_and_drop() {
260        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
261        let addr = listener.local_addr().unwrap();
262        let _accept = tokio::spawn(async move {
263            let (s, _) = listener.accept().await.unwrap();
264            drop(s);
265        });
266        let s = TcpStream::connect(addr).await.unwrap();
267        let conn = Conn::new(
268            Box::new(TcpTransport::new(s, ConnRole::DnodePeerServer)),
269            ConnRole::DnodePeerServer,
270        );
271        let (_tx, rx) = mpsc::channel(1);
272        let _server = DnodeServerConn::new(conn, rx);
273    }
274
275    /// Regression: two requests multiplexed on one peer connection must
276    /// have their replies delivered to their OWN responders. A single
277    /// shared responder slot delivered every reply to the last-enqueued
278    /// request, crossing wires under fan-out. The peer here echoes two
279    /// dnode-framed replies in request order; responder A must receive
280    /// req_id 1's reply and responder B req_id 2's.
281    #[tokio::test]
282    async fn concurrent_requests_route_replies_to_their_own_responders() {
283        use crate::proto::dnode::{dmsg_write, DmsgType, DnodeParser, ParseStep};
284        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
285        let addr = listener.local_addr().unwrap();
286
287        // Mock peer: read two dnode request frames, then reply to each
288        // with a distinct payload in the SAME order, echoing the
289        // request's id in the reply header.
290        let peer = tokio::spawn(async move {
291            use tokio::io::{AsyncReadExt, AsyncWriteExt};
292            let (mut s, _) = listener.accept().await.unwrap();
293            let mut acc = Vec::new();
294            let mut parser = DnodeParser::new();
295            let mut ids = Vec::new();
296            let mut buf = [0u8; 4096];
297            while ids.len() < 2 {
298                let n = s.read(&mut buf).await.unwrap();
299                if n == 0 {
300                    break;
301                }
302                acc.extend_from_slice(&buf[..n]);
303                while let ParseStep::HeaderDone { consumed } = parser.step(&acc) {
304                    let dmsg = parser.take_dmsg();
305                    let total = consumed + dmsg.plen as usize;
306                    if acc.len() < total {
307                        parser.reset();
308                        break;
309                    }
310                    ids.push(dmsg.id);
311                    acc.drain(0..total);
312                    parser.reset();
313                }
314            }
315            // Reply in request order: id[0] -> "+A\r\n", id[1] -> "+B\r\n".
316            let pool = crate::io::mbuf::MbufPool::default();
317            for (i, id) in ids.iter().enumerate() {
318                let payload: &[u8] = if i == 0 { b"+A\r\n" } else { b"+B\r\n" };
319                let mut hb = pool.get();
320                let plen = u32::try_from(payload.len()).unwrap_or(0);
321                dmsg_write(&mut hb, *id, DmsgType::Res, 0, false, None, plen).unwrap();
322                s.write_all(hb.readable()).await.unwrap();
323                s.write_all(payload).await.unwrap();
324            }
325            s.flush().await.unwrap();
326            // Keep the socket open briefly so replies flush.
327            tokio::time::sleep(std::time::Duration::from_millis(200)).await;
328        });
329
330        let stream = TcpStream::connect(addr).await.unwrap();
331        let conn = Conn::new(
332            Box::new(TcpTransport::new(stream, ConnRole::DnodePeerServer)),
333            ConnRole::DnodePeerServer,
334        );
335        let (req_tx, req_rx) = mpsc::channel::<OutboundRequest>(4);
336        let (resp_a_tx, mut resp_a_rx) = mpsc::channel::<OutboundEnvelope>(1);
337        let (resp_b_tx, mut resp_b_rx) = mpsc::channel::<OutboundEnvelope>(1);
338        // Enqueue two requests with DISTINCT responders BEFORE the
339        // driver runs, so both are pending when replies arrive.
340        req_tx
341            .send(OutboundRequest {
342                bytes: b"*1\r\n$4\r\nPING\r\n".to_vec(),
343                req_id: 1,
344                responder: resp_a_tx,
345                span: tracing::Span::current(),
346                ty: DmsgType::Req,
347                target_peer_idx: None,
348            })
349            .await
350            .unwrap();
351        req_tx
352            .send(OutboundRequest {
353                bytes: b"*1\r\n$4\r\nPING\r\n".to_vec(),
354                req_id: 2,
355                responder: resp_b_tx,
356                span: tracing::Span::current(),
357                ty: DmsgType::Req,
358                target_peer_idx: None,
359            })
360            .await
361            .unwrap();
362        drop(req_tx);
363
364        let server = DnodeServerConn::new(conn, req_rx);
365        let driver = tokio::spawn(async move {
366            let _ = tokio::time::timeout(std::time::Duration::from_secs(3), server.run()).await;
367        });
368
369        let a = tokio::time::timeout(std::time::Duration::from_secs(2), resp_a_rx.recv())
370            .await
371            .expect("responder A timed out")
372            .expect("responder A closed");
373        let b = tokio::time::timeout(std::time::Duration::from_secs(2), resp_b_rx.recv())
374            .await
375            .expect("responder B timed out")
376            .expect("responder B closed");
377        // Responder A must receive req_id 1's reply, B req_id 2's.
378        assert_eq!(a.req_id, 1, "responder A got the wrong request's reply");
379        assert_eq!(b.req_id, 2, "responder B got the wrong request's reply");
380        peer.abort();
381        driver.abort();
382    }
383}