Skip to main content

kevy_client/
pipeline.rs

1//! Pipelined batches (v1.14.0): queue N commands client-side, send
2//! them as one write, read N replies in order.
3//!
4//! Unlike [`Connection::multi`](crate::Connection::multi) this is
5//! **not** atomic — the server may interleave other clients' commands
6//! between the batch's — it only removes the per-command round-trip.
7//! Remote-only: the embedded backend has no argv dispatcher (same
8//! rationale as MULTI) and its calls are in-process already; it
9//! answers `Unsupported`.
10//!
11//! ```no_run
12//! use kevy_client::Connection;
13//!
14//! let mut conn = Connection::open("kevy://localhost:6379")?;
15//! let replies = conn.pipeline(|p| {
16//!     p.cmd(&[b"SET", b"a", b"1"]).cmd(&[b"INCR", b"n"]).cmd(&[b"GET", b"a"]);
17//! })?;
18//! assert_eq!(replies.len(), 3);
19//! # Ok::<(), std::io::Error>(())
20//! ```
21
22use std::io;
23
24use kevy_resp::{Reply, encode_command_borrowed};
25
26use crate::Connection;
27
28/// Client-side command buffer for [`Connection::pipeline`]. Each
29/// [`Self::cmd`] appends one RESP-encoded command; the whole buffer is
30/// flushed in a single write.
31#[derive(Debug, Default)]
32pub struct PipelineBuf {
33    buf: Vec<u8>,
34    count: usize,
35    /// Set when a `cmd` call was malformed (empty argv) — surfaced as
36    /// `InvalidInput` at send time so the builder closure can stay
37    /// infallible-chainable.
38    poisoned: bool,
39}
40
41impl PipelineBuf {
42    /// Queue one command — verb + args as raw byte slices, e.g.
43    /// `p.cmd(&[b"SET", key, value])`. Chainable. An empty `parts`
44    /// poisons the buffer and [`Connection::pipeline`] reports it.
45    pub fn cmd(&mut self, parts: &[&[u8]]) -> &mut Self {
46        if parts.is_empty() {
47            self.poisoned = true;
48            return self;
49        }
50        encode_command_borrowed(&mut self.buf, parts);
51        self.count += 1;
52        self
53    }
54
55    /// Number of queued commands.
56    pub fn len(&self) -> usize {
57        self.count
58    }
59
60    /// Whether nothing has been queued.
61    pub fn is_empty(&self) -> bool {
62        self.count == 0
63    }
64}
65
66impl Connection {
67    /// Run a pipelined batch: `build` queues commands into a
68    /// [`PipelineBuf`], then the batch is sent as one write and the
69    /// per-command replies are returned **in queue order**.
70    ///
71    /// Error replies to individual commands come back as
72    /// [`Reply::Error`] entries (they don't abort the batch) — check
73    /// each reply, unlike the typed single-command wraps which map
74    /// server errors to `io::Error`. An empty batch returns `Ok(vec![])`
75    /// without touching the wire.
76    pub fn pipeline(
77        &mut self,
78        build: impl FnOnce(&mut PipelineBuf),
79    ) -> io::Result<Vec<Reply>> {
80        let client = self.remote("pipeline")?;
81        let mut p = PipelineBuf::default();
82        build(&mut p);
83        if p.poisoned {
84            return Err(io::Error::new(
85                io::ErrorKind::InvalidInput,
86                "pipeline: a queued command had an empty argv",
87            ));
88        }
89        if p.count == 0 {
90            return Ok(Vec::new());
91        }
92        client.pipeline_raw(&p.buf, p.count)
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn embedded_pipeline_unsupported() {
102        let mut c = Connection::open("mem://").unwrap();
103        let err = c.pipeline(|p| {
104            p.cmd(&[b"PING"]);
105        });
106        assert_eq!(err.unwrap_err().kind(), io::ErrorKind::Unsupported);
107    }
108
109    #[test]
110    fn pipeline_buf_counts_and_encodes() {
111        let mut p = PipelineBuf::default();
112        assert!(p.is_empty());
113        p.cmd(&[b"SET", b"a", b"1"]).cmd(&[b"GET", b"a"]);
114        assert_eq!(p.len(), 2);
115        assert!(p.buf.starts_with(b"*3\r\n$3\r\nSET\r\n"));
116    }
117}