Skip to main content

dynomite/net/
client.rs

1//! CLIENT-role connection driver.
2//!
3//! The CLIENT role is the engine's view of an inbound connection
4//! from a client (RESP or memcache). The driver:
5//!
6//! 1. Reads bytes from the [`crate::io::reactor::Transport`] into
7//!    the connection's recv mbuf chain.
8//! 2. Drives the appropriate datastore parser
9//!    ([`crate::proto::redis::redis_parse_req`] or
10//!    [`crate::proto::memcache::memcache_parse_req`]) over the
11//!    chain.
12//! 3. Hands every fully-parsed request to the configured
13//!    [`crate::net::Dispatcher`] and waits for a response on the
14//!    per-connection mpsc channel.
15//! 4. Writes the response bytes back to the transport.
16//!
17//! The driver runs a single tokio `select!` per iteration, draining
18//! pending response bytes first so the loop's read / write
19//! arms never block on a saturated peer.
20//!
21//! The driver does not reach into the cluster layer or the entropy
22//! reconciliation directly; both plug in through the [`Dispatcher`]
23//! hook the proxy installs, keeping the driver decoupled from
24//! routing and repair.
25
26use std::sync::Arc;
27use std::time::Duration;
28
29use tokio::io::{AsyncReadExt, AsyncWriteExt};
30use tokio::sync::mpsc;
31
32use crate::conf::DataStore;
33use crate::core::types::MsgId;
34use crate::io::reactor::ConnRole;
35use crate::msg::{response, Msg, MsgParseResult, MsgType};
36use crate::net::conn::Conn;
37use crate::net::dispatcher::{DispatchOutcome, Dispatcher, OutboundEnvelope};
38use crate::net::NetError;
39
40/// Read buffer size for the client driver.
41///
42/// Sized to the typical RESP bulk header so inline GET/SET
43/// commands fit in a single read; larger payloads are appended
44/// across iterations.
45const CLIENT_READ_CHUNK: usize = 4096;
46
47/// Outcome reported by [`client_loop`] when it finishes.
48#[derive(Debug, Clone, Copy, Eq, PartialEq)]
49pub enum ClientLoopOutcome {
50    /// Peer closed (EOF) and queues drained cleanly.
51    Eof,
52    /// Driver was asked to exit by the dispatcher.
53    Cancelled,
54}
55
56/// Sink that applies an inbound cross-node object-replica op to
57/// the local datastore.
58///
59/// The dnode peer-receive loop calls [`Self::apply`] once per
60/// [`DmsgType::RiakReplica`](crate::proto::dnode::DmsgType::RiakReplica)
61/// frame with the frame's opaque payload bytes (the `dyniak`
62/// routing layer owns the payload encoding). The engine does not
63/// interpret the payload; it hands it to the sink, which decodes
64/// it and applies the write to its LOCAL object store.
65///
66/// Applying a replica op is terminal: the sink must NOT re-forward
67/// it to other peers, so a replica write fans out exactly once.
68/// The call is fire-and-forget from the wire's perspective (no
69/// reply frame is produced), matching Riak's eventual-consistency
70/// model where anti-entropy and read-repair reconcile any peer
71/// that missed a write.
72pub trait ReplicaApplySink: Send + Sync {
73    /// Apply the replica op carried by `payload` to the local
74    /// datastore. Errors are logged by the implementor and
75    /// swallowed; the receive loop never blocks on the result.
76    fn apply<'a>(&'a self, payload: &'a [u8]) -> BoxFuture<'a, ()>;
77
78    /// Apply a replica op that expects a reply, returning the reply
79    /// bytes to frame back to the requester as a `Res`, or `None` if
80    /// the op produces no reply (the receive loop then treats it as
81    /// fire-and-forget). This is the read-coordination path: a peer
82    /// fetch reads local state and returns it so the coordinator can
83    /// merge the replica set. The default returns `None`, so a sink
84    /// that only handles writes needs no change.
85    fn apply_query<'a>(&'a self, payload: &'a [u8]) -> BoxFuture<'a, Option<Vec<u8>>> {
86        let _ = payload;
87        Box::pin(async { None })
88    }
89}
90
91/// Boxed future returned by [`ReplicaApplySink::apply`].
92pub type BoxFuture<'a, T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;
93
94/// Client-side request handler bundle.
95///
96/// Built by [`crate::net::proxy::Proxy`] and passed into
97/// [`client_loop`].
98pub struct ClientHandler {
99    dispatcher: Arc<dyn Dispatcher>,
100    response_tx: mpsc::Sender<OutboundEnvelope>,
101    data_store: DataStore,
102    next_msg_id: u64,
103    read_timeout: Option<Duration>,
104    gossip: Option<Arc<crate::cluster::gossip::GossipHandler>>,
105    replica_sink: Option<Arc<dyn ReplicaApplySink>>,
106}
107
108impl ClientHandler {
109    /// Build a client handler.
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// use dynomite::conf::DataStore;
115    /// use dynomite::net::{ClientHandler, NoopDispatcher};
116    /// use std::sync::Arc;
117    /// use tokio::sync::mpsc;
118    /// let (tx, _rx) = mpsc::channel(1);
119    /// let _h = ClientHandler::new(Arc::new(NoopDispatcher), tx, DataStore::Valkey);
120    /// ```
121    #[must_use]
122    pub fn new(
123        dispatcher: Arc<dyn Dispatcher>,
124        response_tx: mpsc::Sender<OutboundEnvelope>,
125        data_store: DataStore,
126    ) -> Self {
127        Self {
128            dispatcher,
129            response_tx,
130            data_store,
131            next_msg_id: 1,
132            read_timeout: None,
133            gossip: None,
134            replica_sink: None,
135        }
136    }
137
138    /// Set the per-read timeout. None disables it.
139    #[must_use]
140    pub fn with_read_timeout(mut self, t: Option<Duration>) -> Self {
141        self.read_timeout = t;
142        self
143    }
144
145    /// Attach a gossip handler. Inbound peer connections served
146    /// through this handler dispatch gossip-class dnode frames
147    /// into the supplied handler instead of the datastore parser.
148    /// Data-plane connections (CLIENT role) leave it `None`.
149    #[must_use]
150    pub fn with_gossip(mut self, gossip: Arc<crate::cluster::gossip::GossipHandler>) -> Self {
151        self.gossip = Some(gossip);
152        self
153    }
154
155    /// Attach a [`ReplicaApplySink`]. Inbound peer connections
156    /// served through this handler apply
157    /// [`DmsgType::RiakReplica`](crate::proto::dnode::DmsgType::RiakReplica)
158    /// frames to the local datastore via the sink instead of
159    /// routing them through the datastore parser, and never
160    /// re-forward them.
161    #[must_use]
162    pub fn with_replica_sink(mut self, sink: Arc<dyn ReplicaApplySink>) -> Self {
163        self.replica_sink = Some(sink);
164        self
165    }
166
167    /// Borrow the attached replica-apply sink, if any.
168    #[must_use]
169    pub fn replica_sink(&self) -> Option<&Arc<dyn ReplicaApplySink>> {
170        self.replica_sink.as_ref()
171    }
172
173    /// Borrow the attached gossip handler, if any.
174    #[must_use]
175    pub fn gossip(&self) -> Option<&Arc<crate::cluster::gossip::GossipHandler>> {
176        self.gossip.as_ref()
177    }
178
179    /// Datastore the handler parses.
180    #[must_use]
181    pub fn data_store(&self) -> DataStore {
182        self.data_store
183    }
184
185    /// Borrow the dispatcher this handler routes parsed requests
186    /// into. Exposed so role-specific drivers (CLIENT,
187    /// DNODE_PEER_CLIENT) can share the same dispatch contract.
188    ///
189    /// # Examples
190    ///
191    /// ```
192    /// use dynomite::conf::DataStore;
193    /// use dynomite::net::{ClientHandler, NoopDispatcher};
194    /// use std::sync::Arc;
195    /// use tokio::sync::mpsc;
196    /// let (tx, _rx) = mpsc::channel(1);
197    /// let h = ClientHandler::new(Arc::new(NoopDispatcher), tx, DataStore::Valkey);
198    /// let _ = h.dispatcher();
199    /// ```
200    #[must_use]
201    pub fn dispatcher(&self) -> &Arc<dyn Dispatcher> {
202        &self.dispatcher
203    }
204
205    /// Borrow the per-connection response sender. The dispatcher
206    /// uses a clone of this channel to push asynchronously-produced
207    /// responses back to the FSM.
208    ///
209    /// # Examples
210    ///
211    /// ```
212    /// use dynomite::conf::DataStore;
213    /// use dynomite::net::{ClientHandler, NoopDispatcher};
214    /// use std::sync::Arc;
215    /// use tokio::sync::mpsc;
216    /// let (tx, _rx) = mpsc::channel(1);
217    /// let h = ClientHandler::new(Arc::new(NoopDispatcher), tx, DataStore::Valkey);
218    /// let _clone = h.response_tx().clone();
219    /// ```
220    #[must_use]
221    pub fn response_tx(&self) -> &mpsc::Sender<OutboundEnvelope> {
222        &self.response_tx
223    }
224
225    fn alloc_msg_id(&mut self) -> MsgId {
226        let id = self.next_msg_id;
227        self.next_msg_id = self.next_msg_id.wrapping_add(1).max(1);
228        id
229    }
230}
231
232/// Drive the client FSM until the peer closes or the dispatcher
233/// asks the driver to exit.
234///
235/// `rx` receives responses produced by the dispatcher; the driver
236/// writes the response bytes to the transport in the order it
237/// received them.
238///
239/// # Errors
240/// Any transport-level error is returned. Parse errors are surfaced
241/// as [`NetError::Parse`] and end the loop after sending a synthetic
242/// error response when possible.
243#[tracing::instrument(
244    name = "client_loop",
245    skip_all,
246    fields(
247        role = ?conn.role(),
248        peer = tracing::field::Empty,
249    ),
250)]
251pub async fn client_loop(
252    mut conn: Conn,
253    mut handler: ClientHandler,
254    mut rx: mpsc::Receiver<OutboundEnvelope>,
255) -> Result<(), NetError> {
256    debug_assert!(matches!(
257        conn.role(),
258        ConnRole::Client | ConnRole::DnodePeerClient
259    ));
260
261    let mut read_buf = vec![0u8; CLIENT_READ_CHUNK];
262    let mut accumulated: Vec<u8> = Vec::new();
263    let mut pending_writes: Vec<u8> = Vec::new();
264
265    loop {
266        // Flush any buffered response bytes first so the loop
267        // exit conditions never block on a full peer.
268        if !pending_writes.is_empty() {
269            let transport = conn.transport_mut().ok_or(NetError::Closed)?;
270            transport.write_all(&pending_writes).await?;
271            conn.record_send(pending_writes.len());
272            pending_writes.clear();
273        }
274
275        if conn.is_eof() && conn.imsg_q().is_empty() && conn.omsg_q().is_empty() {
276            conn.set_done();
277            return Ok(());
278        }
279
280        let read_fut = async {
281            let n = match conn.transport_mut() {
282                Some(t) => t.read(&mut read_buf).await,
283                None => return Ok::<usize, std::io::Error>(0),
284            };
285            n
286        };
287
288        tokio::select! {
289            res = read_fut => {
290                let n = res?;
291                if n == 0 {
292                    conn.set_eof();
293                    if conn.omsg_q().is_empty() {
294                        conn.set_done();
295                        return Ok(());
296                    }
297                    continue;
298                }
299                conn.record_recv(n);
300                accumulated.extend_from_slice(&read_buf[..n]);
301                drive_parser(&mut conn, &mut handler, &mut accumulated).await?;
302            }
303            Some(env) = rx.recv() => {
304                handle_response(&mut conn, &env, &mut pending_writes);
305            }
306            else => {
307                conn.set_done();
308                return Ok(());
309            }
310        }
311    }
312}
313
314#[tracing::instrument(
315    name = "client.parse_loop",
316    skip_all,
317    fields(accumulated = accumulated.len()),
318)]
319async fn drive_parser(
320    conn: &mut Conn,
321    handler: &mut ClientHandler,
322    accumulated: &mut Vec<u8>,
323) -> Result<(), NetError> {
324    use crate::proto::memcache::memcache_parse_req;
325    use crate::proto::redis::redis_parse_req;
326
327    while !accumulated.is_empty() {
328        let id = handler.alloc_msg_id();
329        let mut msg = Msg::new(id, MsgType::Unknown, true);
330        let consumed_before = msg.parser_pos();
331        let parse_result = match handler.data_store {
332            DataStore::Valkey | DataStore::Dyniak => redis_parse_req(&mut msg, accumulated),
333            DataStore::Memcache => memcache_parse_req(&mut msg, accumulated),
334        };
335        match parse_result {
336            MsgParseResult::Ok => {
337                let consumed = msg.parser_pos();
338                if consumed == 0 {
339                    return Err(NetError::Parse(
340                        "parser reported Ok with no bytes consumed".to_string(),
341                    ));
342                }
343                // Per-request span - one is created for every
344                // fully-parsed inbound message. Cross-task work
345                // (dispatch.plan, backend.send / parse, peer.send /
346                // parse, client.send) attaches as children via the
347                // captured `tracing::Span` on the OutboundRequest /
348                // OutboundEnvelope envelopes.
349                let req_span = tracing::info_span!(
350                    "client.parse",
351                    msg_id = msg.id(),
352                    msg_type = ?msg.ty(),
353                    bytes = consumed,
354                );
355                let was_quit = msg.flags().quit;
356                let quit_msg_id = if was_quit { Some(msg.id()) } else { None };
357                let inline_send: Option<OutboundEnvelope> = req_span.in_scope(|| {
358                    // Carry the consumed wire bytes inside the
359                    // msg so the dispatcher can forward them to a
360                    // backend without having to re-encode. The bytes
361                    // live on the msg's own mbuf chain across
362                    // recv -> filter -> forward.
363                    let pool = conn.mbuf_pool().clone();
364                    let mut buf = pool.get();
365                    buf.recv(&accumulated[..consumed]);
366                    msg.mbufs_mut().push_back(buf);
367                    msg.recompute_mlen();
368                    accumulated.drain(0..consumed);
369                    let _ = consumed_before;
370                    conn.outstanding_mut().insert(msg.id(), msg.id());
371                    // The placeholder enqueue cannot fail under
372                    // normal operation; if it does we surface it
373                    // via the outer `?`.
374                    conn.enqueue_out(Msg::new(msg.id(), msg.ty(), true))
375                        .map_err(|e: NetError| e)?;
376                    let outcome = handler
377                        .dispatcher
378                        .dispatch(msg, handler.response_tx.clone());
379                    let inline = match outcome {
380                        DispatchOutcome::Pending | DispatchOutcome::Drop => None,
381                        DispatchOutcome::Inline(rsp) | DispatchOutcome::Error(rsp) => {
382                            Some(OutboundEnvelope {
383                                req_id: rsp.id(),
384                                rsp,
385                                span: tracing::Span::current(),
386                                source_peer_idx: None,
387                            })
388                        }
389                    };
390                    Ok::<Option<OutboundEnvelope>, NetError>(inline)
391                })?;
392                if let Some(env) = inline_send {
393                    let _ = handler.response_tx.send(env).await;
394                }
395                if let Some(qid) = quit_msg_id {
396                    // Real Redis replies `+OK\r\n` to QUIT and then
397                    // closes the client connection. Synthesize the
398                    // reply here (the dispatcher returned `Drop`
399                    // because there is no key to route) and send it
400                    // through the same `response_tx` used by every
401                    // other reply, which pops the placeholder we
402                    // pushed onto `omsg_q` above. Without this the
403                    // outer client loop's exit condition
404                    // (`omsg_q.is_empty()`) is never met and the
405                    // connection deadlocks until the kernel times
406                    // out the read.
407                    let pool = conn.mbuf_pool().clone();
408                    let mut anchor = Msg::new(qid, MsgType::ReqRedisQuit, true);
409                    anchor.set_parent_id(qid);
410                    let rsp = response::make_simple_redis(&anchor, &pool, b"+OK\r\n");
411                    let env = OutboundEnvelope {
412                        req_id: qid,
413                        rsp,
414                        span: req_span.clone(),
415                        source_peer_idx: None,
416                    };
417                    let _ = handler.response_tx.send(env).await;
418                    // Close the connection after replying.
419                    conn.set_eof();
420                    return Ok(());
421                }
422            }
423            MsgParseResult::Again
424            | MsgParseResult::Repair
425            | MsgParseResult::Fragment
426            | MsgParseResult::Noop => {
427                let consumed = msg.parser_pos();
428                if consumed > 0 {
429                    accumulated.drain(0..consumed);
430                } else {
431                    return Ok(());
432                }
433            }
434            MsgParseResult::Error | MsgParseResult::OomError | MsgParseResult::DynoConfig => {
435                return Err(NetError::Parse(format!("{parse_result:?}")));
436            }
437        }
438    }
439    Ok(())
440}
441
442fn handle_response(conn: &mut Conn, env: &OutboundEnvelope, pending: &mut Vec<u8>) {
443    let _enter = env.span.enter();
444    let bytes_len: usize = env.rsp.mbufs().iter().map(|b| b.readable().len()).sum();
445    let _send_span =
446        tracing::info_span!("client.send", req_id = env.req_id, bytes = bytes_len,).entered();
447    for buf in env.rsp.mbufs() {
448        pending.extend_from_slice(buf.readable());
449    }
450    // Pop the matching outstanding entry.
451    conn.outstanding_mut().remove(&env.req_id);
452    // Pop the placeholder we pushed on enqueue.
453    if let Some(front) = conn.omsg_q_mut().front() {
454        if front.id() == env.req_id {
455            let _ = conn.omsg_q_mut().pop_front();
456        }
457    }
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463
464    #[test]
465    fn alloc_msg_id_is_monotonic() {
466        let (tx, _rx) = mpsc::channel(1);
467        let mut h = ClientHandler::new(Arc::new(crate::net::NoopDispatcher), tx, DataStore::Valkey);
468        let a = h.alloc_msg_id();
469        let b = h.alloc_msg_id();
470        assert_eq!(a + 1, b);
471    }
472}