Skip to main content

ringline_memcache/
lib.rs

1//! ringline-native Memcache client for use inside the ringline async runtime.
2//!
3//! This client wraps a [`ringline::ConnCtx`] and provides typed Memcache command
4//! methods that use `with_bytes()` + `ResponseBytes::parse()` for zero-copy
5//! incremental parsing. It is designed for single-threaded, single-connection
6//! use within ringline's `AsyncEventHandler::on_start()` or connection tasks.
7//!
8//! All key and value parameters accept `impl AsRef<[u8]>`, so you can pass
9//! `&str`, `String`, `&[u8]`, `Vec<u8>`, `Bytes`, etc.
10//!
11//! # Sequential API (Simple)
12//!
13//! The basic API sends one command and awaits its response:
14//!
15//! ```no_run
16//! use ringline::ConnCtx;
17//! use ringline_memcache::Client;
18//!
19//! async fn example(conn: ConnCtx) -> Result<(), ringline_memcache::Error> {
20//!     let mut client = Client::new(conn);
21//!     client.set("hello", "world").await?;
22//!     let val = client.get("hello").await?;
23//!     assert_eq!(val.unwrap().data.as_ref(), b"world");
24//!     Ok(())
25//! }
26//! ```
27//!
28//! # Fire/Recv Pipelining API (High Throughput)
29//!
30//! For higher throughput, use the fire/recv pattern to pipeline multiple
31//! commands without waiting for each response:
32//!
33//! ```text
34//! ┌─────────────────────────────────────────────────────────────────┐
35//! │                        Application                              │
36//! │                                                                 │
37//! │   client.fire_get("key1", 1)?;  ──┐                            │
38//! │   client.fire_get("key2", 2)?;  ──┼───→ [single TCP send]      │
39//! │   client.fire_get("key3", 3)?;  ──┘                            │
40//! │                              │                                │
41//! │                              ▼                                │
42//! │                    [Memcache processes]                       │
43//! │                              │                                │
44//! │                              ▼                                │
45//! │   let r1 = client.recv().await?;  ←── [response 1, user_data=1]│
46//! │   let r2 = client.recv().await?;  ←── [response 2, user_data=2]│
47//! │   let r3 = client.recv().await?;  ←── [response 3, user_data=3]│
48//! │                                                                 │
49//! └─────────────────────────────────────────────────────────────────┘
50//! ```
51//!
52//! ```no_run
53//! use ringline::ConnCtx;
54//! use ringline_memcache::{Client, CompletedOp};
55//!
56//! async fn pipelined_example(conn: ConnCtx) -> Result<(), ringline_memcache::Error> {
57//!     let mut client = Client::new(conn);
58//!
59//!     // Fire multiple requests (synchronous, non-blocking)
60//!     client.fire_get(b"session:abc", 1)?;
61//!     client.fire_get(b"session:def", 2)?;
62//!     client.fire_get(b"session:ghi", 3)?;
63//!
64//!     // Recv responses in order (async, blocks until each arrives)
65//!     match client.recv().await? {
66//!         CompletedOp::Get { result, user_data, .. } => {
67//!             assert_eq!(user_data, 1);
68//!             let value = result?; // Option<Value>
69//!             println!("session:abc = {:?}", value);
70//!         }
71//!         _ => unreachable!(),
72//!     }
73//!
74//!     // Continue with remaining responses...
75//!     match client.recv().await? {
76//!         CompletedOp::Get { result: _, user_data, .. } => {
77//!             assert_eq!(user_data, 2);
78//!         }
79//!         _ => unreachable!(),
80//!     }
81//!
82//!     match client.recv().await? {
83//!         CompletedOp::Get { result: _, user_data, .. } => {
84//!             assert_eq!(user_data, 3);
85//!         }
86//!         _ => unreachable!(),
87//!     }
88//!
89//!     Ok(())
90//! }
91//! ```
92//!
93//! ## Fire/Recv Benefits
94//!
95//! - **Overlaps network RTT**: All commands sent before any response received
96//! - **Better TCP utilization**: Multiple commands coalesced into fewer segments
97//! - **Correlation via `user_data`**: Attach opaque `u64` to each fire, returned on recv
98//! - **Zero-copy values**: Responses are `Bytes::slice()` into the recv accumulator
99//!
100//! ## Available Fire Methods
101//!
102//! - [`Client::fire_get()`] — GET command
103//! - [`Client::fire_set()`] — SET command
104//! - [`Client::fire_set_with_guard()`] — SET with zero-copy value
105//! - [`Client::fire_delete()`] — DELETE command
106//!
107//! ## Important Notes
108//!
109//! - Responses must be consumed in FIFO order (protocol guarantee)
110//! - `recv()` returns [`Error::NoPending`] if called with no in-flight requests
111//! - Timing is zero-overhead when no callbacks/metrics are configured
112//!
113//! # Copy Semantics
114//!
115//! | Path | Copies | Mechanism |
116//! |------|--------|-----------|
117//! | **Recv (values)** | **0** | `with_bytes()` + `ResponseBytes::parse()`. Keys and values are `Bytes::slice()` references into the accumulator — zero allocation, O(1) refcount. |
118//! | **Send (commands)** | 1 | Requests are encoded into a reusable per-client buffer (no per-request allocation), then `conn.send()` copies into the send pool. |
119//! | **Send (SET value, guard)** | 0 (value) | [`Client::set_with_guard`]: prefix+suffix copied to pool, value stays in-place via `SendGuard`. |
120//!
121//! TLS connections add encryption copies on the send path regardless of
122//! `SendGuard` usage.
123
124pub mod pool;
125pub mod sharded;
126pub use pool::{Pool, PoolConfig};
127pub use sharded::{ShardedClient, ShardedConfig};
128
129use std::cell::Cell;
130use std::collections::VecDeque;
131use std::io;
132use std::time::Instant;
133
134use bytes::Bytes;
135use memcache_proto::{Request as McRequest, ResponseBytes as McResponseBytes};
136use ringline::{ConnCtx, GuardBox, ParseResult, SendGuard};
137
138/// Callback type invoked after each command completes.
139type ResultCallback = Box<dyn Fn(&CommandResult)>;
140
141/// Maximum guards per scatter-gather send (matches ringline core limit).
142const MAX_FLUSH_GUARDS: usize = 8;
143/// Maximum iovecs a full guard batch needs: each guard contributes an iovec
144/// plus at most one copied-run iovec before it, plus one trailing copied run
145/// (2g+1 = 17). Well under the ringline core limit of 32. Was incorrectly 8,
146/// which made guard batches flush at 3 guards instead of MAX_FLUSH_GUARDS.
147const MAX_FLUSH_IOVECS: usize = 2 * MAX_FLUSH_GUARDS + 1;
148/// Default [`ClientBuilder::zc_threshold`]. Matches the runtime
149/// `Config::send_zc_threshold` default so small guarded SETs fold into the
150/// coalescing copy path by default.
151const DEFAULT_ZC_THRESHOLD: u32 = 4096;
152
153// -- Error -------------------------------------------------------------------
154
155/// Errors returned by the ringline Memcache client.
156///
157/// Marked `#[non_exhaustive]` because the crate is still evolving and new
158/// transport / protocol error kinds are expected. Downstream `match`
159/// blocks must include a wildcard arm.
160#[derive(Debug, thiserror::Error)]
161#[non_exhaustive]
162pub enum Error {
163    /// The connection was closed before a response was received.
164    #[error("connection closed")]
165    ConnectionClosed,
166
167    /// The server returned an error response (ERROR, CLIENT_ERROR, SERVER_ERROR).
168    #[error("memcache error: {0}")]
169    Memcache(String),
170
171    /// The response type did not match the expected type for the command.
172    #[error("unexpected response")]
173    UnexpectedResponse,
174
175    /// Memcache protocol parse error.
176    #[error("protocol error: {0}")]
177    Protocol(#[from] memcache_proto::ParseError),
178
179    /// I/O error during send.
180    #[error("io error: {0}")]
181    Io(#[from] io::Error),
182
183    /// All connections in the pool are down and reconnection failed.
184    #[error("all connections failed")]
185    AllConnectionsFailed,
186
187    /// `recv()` called with no pending fire operations.
188    #[error("no pending operations")]
189    NoPending,
190
191    /// The in-flight pending-op queue reached `max_in_flight`. Drain via
192    /// `recv()` before issuing more `fire_*` calls. Configurable via
193    /// [`ClientBuilder::max_in_flight`].
194    #[error("too many in-flight operations")]
195    TooManyInFlight,
196
197    /// Key exceeds memcache's 250-byte cap ([`MAX_KEY_LEN`]). The request
198    /// is rejected client-side instead of being transmitted; otherwise the
199    /// server would reply with `CLIENT_ERROR` after consuming pool /
200    /// pending-queue capacity for a doomed command.
201    #[error("key too long (max 250 bytes)")]
202    KeyTooLong,
203
204    /// A streaming [`set_stream`](Client::set_stream) value source produced a
205    /// different number of bytes than the declared length. Because the command
206    /// header (which declares the length) is already on the wire, the framing is
207    /// now irrecoverably desynced, so the connection is closed — matching the
208    /// read-side poison rule.
209    #[error("streaming value length mismatch")]
210    LengthMismatch,
211}
212
213/// Maximum key length per memcache text-protocol spec.
214pub const MAX_KEY_LEN: usize = 250;
215
216#[inline]
217fn validate_key(key: &[u8]) -> Result<(), Error> {
218    if key.len() > MAX_KEY_LEN {
219        Err(Error::KeyTooLong)
220    } else {
221        Ok(())
222    }
223}
224
225// -- Value types -------------------------------------------------------------
226
227/// A value returned from a single-key GET command.
228#[derive(Debug, Clone)]
229pub struct Value {
230    /// The cached data.
231    pub data: Bytes,
232    /// Flags stored with the item.
233    pub flags: u32,
234}
235
236/// A value returned from a multi-key GET command, including the key.
237#[derive(Debug, Clone)]
238pub struct GetValue {
239    /// The key for this value.
240    pub key: Bytes,
241    /// The cached data.
242    pub data: Bytes,
243    /// Flags stored with the item.
244    pub flags: u32,
245    /// CAS unique token (present when the server returns it via `gets`).
246    pub cas: Option<u64>,
247}
248
249// ── Command types ───────────────────────────────────────────────────────
250
251/// The type of Memcache command that completed.
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253#[non_exhaustive]
254pub enum CommandType {
255    Get,
256    Set,
257    Delete,
258    Other,
259}
260
261/// Result metadata for a completed command, passed to the `on_result` callback.
262#[derive(Debug, Clone)]
263pub struct CommandResult {
264    /// The command type.
265    pub command: CommandType,
266    /// Latency in nanoseconds (send → response parsed).
267    pub latency_ns: u64,
268    /// For GET: `Some(true)` = hit, `Some(false)` = miss. `None` for others.
269    pub hit: Option<bool>,
270    /// Whether the command succeeded (no error response).
271    pub success: bool,
272    /// Time-to-first-byte in nanoseconds (not available in sequential mode).
273    pub ttfb_ns: Option<u64>,
274    /// Bytes transmitted for this command (protocol-encoded request size).
275    pub tx_bytes: u32,
276    /// Bytes received for this command (protocol-encoded response size).
277    pub rx_bytes: u32,
278}
279
280// ── ClientMetrics ───────────────────────────────────────────────────────
281
282/// Built-in histogram-based metrics, available when the `metrics` feature is
283/// enabled. Not registered globally — the caller decides how to expose them.
284#[cfg(feature = "metrics")]
285pub struct ClientMetrics {
286    /// Overall request latency histogram.
287    pub latency: histogram::Histogram,
288    /// GET latency histogram.
289    pub get_latency: histogram::Histogram,
290    /// SET latency histogram.
291    pub set_latency: histogram::Histogram,
292    /// DEL latency histogram.
293    pub del_latency: histogram::Histogram,
294    /// Total requests completed.
295    pub requests: u64,
296    /// Total errors.
297    pub errors: u64,
298    /// Total GET hits.
299    pub hits: u64,
300    /// Total GET misses.
301    pub misses: u64,
302}
303
304#[cfg(feature = "metrics")]
305impl ClientMetrics {
306    fn new() -> Self {
307        Self {
308            latency: histogram::Histogram::new(7, 64).unwrap(),
309            get_latency: histogram::Histogram::new(7, 64).unwrap(),
310            set_latency: histogram::Histogram::new(7, 64).unwrap(),
311            del_latency: histogram::Histogram::new(7, 64).unwrap(),
312            requests: 0,
313            errors: 0,
314            hits: 0,
315            misses: 0,
316        }
317    }
318
319    fn record(&mut self, result: &CommandResult) {
320        self.requests += 1;
321        let _ = self.latency.increment(result.latency_ns);
322
323        if !result.success {
324            self.errors += 1;
325        }
326
327        match result.command {
328            CommandType::Get => {
329                let _ = self.get_latency.increment(result.latency_ns);
330                match result.hit {
331                    Some(true) => self.hits += 1,
332                    Some(false) => self.misses += 1,
333                    None => {}
334                }
335            }
336            CommandType::Set => {
337                let _ = self.set_latency.increment(result.latency_ns);
338            }
339            CommandType::Delete => {
340                let _ = self.del_latency.increment(result.latency_ns);
341            }
342            _ => {}
343        }
344    }
345}
346
347// ── Pending operation state ─────────────────────────────────────────────
348
349enum PendingOpKind {
350    Get,
351    Set,
352    Delete,
353}
354
355struct PendingOp {
356    kind: PendingOpKind,
357    send_ts: u64,
358    start: Option<Instant>,
359    user_data: u64,
360    tx_bytes: u32,
361}
362
363/// A completed fire/recv operation with its result.
364#[non_exhaustive]
365pub enum CompletedOp {
366    /// GET completed.
367    Get {
368        result: Result<Option<Value>, Error>,
369        user_data: u64,
370        latency_ns: u64,
371    },
372    /// SET completed.
373    Set {
374        result: Result<(), Error>,
375        user_data: u64,
376        latency_ns: u64,
377    },
378    /// DELETE completed.
379    Delete {
380        result: Result<bool, Error>,
381        user_data: u64,
382        latency_ns: u64,
383    },
384}
385
386// ── ClientBuilder ───────────────────────────────────────────────────────
387
388/// Builder for creating a [`Client`] with per-request callbacks and metrics.
389pub struct ClientBuilder {
390    conn: ConnCtx,
391    on_result: Option<ResultCallback>,
392    max_batch_size: usize,
393    max_in_flight: usize,
394    zc_threshold: u32,
395    #[cfg(feature = "timestamps")]
396    use_kernel_ts: bool,
397    #[cfg(feature = "metrics")]
398    with_metrics: bool,
399}
400
401impl ClientBuilder {
402    pub(crate) fn new(conn: ConnCtx) -> Self {
403        Self {
404            conn,
405            on_result: None,
406            max_batch_size: 1,
407            max_in_flight: usize::MAX,
408            zc_threshold: DEFAULT_ZC_THRESHOLD,
409            #[cfg(feature = "timestamps")]
410            use_kernel_ts: false,
411            #[cfg(feature = "metrics")]
412            with_metrics: false,
413        }
414    }
415
416    /// Set the zero-copy guard threshold (in bytes). Guard values passed to
417    /// [`fire_set_with_guard`](Client::fire_set_with_guard) *smaller* than
418    /// this are copied into the coalescing send buffer so they batch like
419    /// plain SETs instead of taking the scatter-gather guard path (which
420    /// flushes every few ops). `0` = always use the guard path.
421    ///
422    /// Defaults to 4096. Should generally match the runtime
423    /// `Config::send_zc_threshold`.
424    pub fn zc_threshold(mut self, bytes: u32) -> Self {
425        self.zc_threshold = bytes;
426        self
427    }
428
429    /// Configure the maximum number of in-flight `fire_*` operations.
430    /// `fire_*` returns [`Error::TooManyInFlight`] past it. Defaults to
431    /// `usize::MAX` (unbounded). Set a bounded value on any server that
432    /// issues `fire_*` faster than `recv()` consumes.
433    pub fn max_in_flight(mut self, n: usize) -> Self {
434        self.max_in_flight = n;
435        self
436    }
437
438    /// Register a callback invoked after each command completes.
439    pub fn on_result<F: Fn(&CommandResult) + 'static>(mut self, f: F) -> Self {
440        self.on_result = Some(Box::new(f));
441        self
442    }
443
444    /// Enable kernel SO_TIMESTAMPING for latency measurement (requires `timestamps` feature).
445    #[cfg(feature = "timestamps")]
446    pub fn kernel_timestamps(mut self, enabled: bool) -> Self {
447        self.use_kernel_ts = enabled;
448        self
449    }
450
451    /// Set the maximum number of `fire_*` commands to coalesce into a single
452    /// send. Default is 1 (each `fire_*` sends immediately, matching
453    /// pre-coalescing behavior). Set higher for pipelined workloads to batch
454    /// multiple commands into fewer TCP segments.
455    ///
456    /// # Panics
457    ///
458    /// Panics if `n` is 0.
459    pub fn max_batch_size(mut self, n: usize) -> Self {
460        assert!(n > 0, "max_batch_size must be >= 1");
461        self.max_batch_size = n;
462        self
463    }
464
465    /// Enable built-in histogram tracking (requires `metrics` feature).
466    #[cfg(feature = "metrics")]
467    pub fn with_metrics(mut self) -> Self {
468        self.with_metrics = true;
469        self
470    }
471
472    /// Build the client.
473    pub fn build(self) -> Client {
474        Client {
475            conn: self.conn,
476            on_result: self.on_result,
477            pending: VecDeque::with_capacity(16),
478            last_rx_bytes: Cell::new(0),
479            write_buf: Vec::new(),
480            write_guards: Vec::new(),
481            flushed_count: 0,
482            max_batch_size: self.max_batch_size,
483            max_in_flight: self.max_in_flight,
484            zc_threshold: self.zc_threshold,
485            buffered_ops: 0,
486            encode_buf: Vec::new(),
487            #[cfg(feature = "timestamps")]
488            use_kernel_ts: self.use_kernel_ts,
489            #[cfg(feature = "metrics")]
490            metrics: if self.with_metrics {
491                Some(ClientMetrics::new())
492            } else {
493                None
494            },
495        }
496    }
497}
498
499// -- Client ------------------------------------------------------------------
500
501/// A ringline-native Memcache client wrapping a single connection.
502///
503/// `Client::new(conn)` creates a zero-overhead client with no callbacks or
504/// metrics. Use `Client::builder(conn)` to configure per-request callbacks,
505/// kernel timestamps, and built-in histogram tracking.
506pub struct Client {
507    conn: ConnCtx,
508    on_result: Option<ResultCallback>,
509    pending: VecDeque<PendingOp>,
510    last_rx_bytes: Cell<u32>,
511    /// Write buffer for coalescing `fire_*` commands. Contains all copy data
512    /// (command framing, prefixes, suffixes, non-guard values). Guard values
513    /// are stored separately in `write_guards` with byte offsets into this buffer.
514    write_buf: Vec<u8>,
515    /// Zero-copy guards pending flush. Each entry is `(offset, guard)` where
516    /// `offset` is the byte position in `write_buf` where the guard value
517    /// should be inserted in the byte stream.
518    write_guards: Vec<(usize, GuardBox)>,
519    /// Number of pending ops whose send_ts has been finalized (at flush time).
520    flushed_count: usize,
521    /// Maximum `fire_*` commands to coalesce before flushing. 1 = send each
522    /// command immediately (default, matching pre-coalescing behavior).
523    max_batch_size: usize,
524    /// Cap on `pending.len()`; `fire_*` returns `Error::TooManyInFlight`
525    /// past it. `usize::MAX` (default) disables.
526    max_in_flight: usize,
527    /// Guard values smaller than this (bytes) are folded into the coalescing
528    /// copy path by `fire_set_with_guard` instead of taking the
529    /// scatter-gather guard path. `0` disables the fold.
530    zc_threshold: u32,
531    /// Number of ops buffered in `write_buf` that have not yet been flushed.
532    buffered_ops: usize,
533    /// Reusable scratch buffer for encoding requests. Cleared before each
534    /// use; keeps its capacity across requests so steady-state encoding is
535    /// allocation-free.
536    encode_buf: Vec<u8>,
537    #[cfg(feature = "timestamps")]
538    use_kernel_ts: bool,
539    #[cfg(feature = "metrics")]
540    metrics: Option<ClientMetrics>,
541}
542
543impl Client {
544    /// Create a new client wrapping an established connection.
545    ///
546    /// No callbacks, no metrics, no kernel timestamps — zero overhead.
547    pub fn new(conn: ConnCtx) -> Self {
548        Self {
549            conn,
550            on_result: None,
551            pending: VecDeque::new(),
552            last_rx_bytes: Cell::new(0),
553            write_buf: Vec::new(),
554            write_guards: Vec::new(),
555            flushed_count: 0,
556            max_batch_size: 1,
557            max_in_flight: usize::MAX,
558            zc_threshold: DEFAULT_ZC_THRESHOLD,
559            buffered_ops: 0,
560            encode_buf: Vec::new(),
561            #[cfg(feature = "timestamps")]
562            use_kernel_ts: false,
563            #[cfg(feature = "metrics")]
564            metrics: None,
565        }
566    }
567
568    /// Create a builder for a client with per-request callbacks.
569    pub fn builder(conn: ConnCtx) -> ClientBuilder {
570        ClientBuilder::new(conn)
571    }
572
573    /// Returns the underlying connection context.
574    pub fn conn(&self) -> ConnCtx {
575        self.conn
576    }
577
578    /// Returns a reference to the built-in metrics, if enabled.
579    #[cfg(feature = "metrics")]
580    pub fn metrics(&self) -> Option<&ClientMetrics> {
581        self.metrics.as_ref()
582    }
583
584    /// Returns a mutable reference to the built-in metrics, if enabled.
585    #[cfg(feature = "metrics")]
586    pub fn metrics_mut(&mut self) -> Option<&mut ClientMetrics> {
587        self.metrics.as_mut()
588    }
589
590    // ── Timing helpers (private) ────────────────────────────────────────
591
592    #[inline]
593    fn is_instrumented(&self) -> bool {
594        if self.on_result.is_some() {
595            return true;
596        }
597        #[cfg(feature = "metrics")]
598        if self.metrics.is_some() {
599            return true;
600        }
601        false
602    }
603
604    #[cfg(feature = "timestamps")]
605    #[inline]
606    fn send_timestamp(&self) -> u64 {
607        if self.use_kernel_ts {
608            now_realtime_ns()
609        } else {
610            0
611        }
612    }
613
614    #[cfg(not(feature = "timestamps"))]
615    #[inline]
616    fn send_timestamp(&self) -> u64 {
617        0
618    }
619
620    #[cfg(feature = "timestamps")]
621    #[inline]
622    fn finish_timing(&self, send_ts: u64, start: Instant) -> u64 {
623        if self.use_kernel_ts {
624            let recv_ts = self.conn.recv_timestamp();
625            if recv_ts > 0 && recv_ts > send_ts {
626                return recv_ts - send_ts;
627            }
628        }
629        start.elapsed().as_nanos() as u64
630    }
631
632    #[cfg(not(feature = "timestamps"))]
633    #[inline]
634    fn finish_timing(&self, _send_ts: u64, start: Instant) -> u64 {
635        start.elapsed().as_nanos() as u64
636    }
637
638    fn record(&mut self, result: &CommandResult) {
639        if let Some(ref cb) = self.on_result {
640            cb(result);
641        }
642        #[cfg(feature = "metrics")]
643        if let Some(ref mut m) = self.metrics {
644            m.record(result);
645        }
646    }
647
648    // ── Fire/recv pipelining API ─────────────────────────────────────────
649
650    #[inline]
651    fn timing_start(&self) -> (u64, Option<Instant>) {
652        #[cfg(feature = "timestamps")]
653        {
654            if self.is_instrumented() {
655                (self.send_timestamp(), Some(Instant::now()))
656            } else {
657                (0, None)
658            }
659        }
660        #[cfg(not(feature = "timestamps"))]
661        {
662            // When timestamps feature is disabled, only use Instant::now() if callbacks are registered
663            if self.on_result.is_some() {
664                (0, Some(Instant::now()))
665            } else {
666                (0, None)
667            }
668        }
669    }
670
671    /// `timing_start()` for `fire_*` call sites, evaluated *after* the op
672    /// has been buffered/sent. When this op is buffered behind another op
673    /// (`buffered_ops > 1`), `flush()` will unconditionally re-stamp
674    /// `send_ts`/`start` for every op in the batch, so the fire-time clock
675    /// read would be discarded — skip it. `buffered_ops` is monotonic
676    /// between flushes, so if it is > 1 now, the rewriting branch in
677    /// `flush()` (`buffered_ops > 1`) is guaranteed to run for this op.
678    #[inline]
679    fn timing_start_buffered(&self) -> (u64, Option<Instant>) {
680        if self.buffered_ops >= 1 {
681            (0, None)
682        } else {
683            self.timing_start()
684        }
685    }
686
687    #[cfg(feature = "timestamps")]
688    #[inline]
689    fn compute_ttfb(&self, send_ts: u64) -> Option<u64> {
690        if self.use_kernel_ts {
691            let recv_ts = self.conn.recv_timestamp();
692            if recv_ts > 0 && recv_ts > send_ts {
693                return Some(recv_ts - send_ts);
694            }
695        }
696        None
697    }
698
699    #[cfg(not(feature = "timestamps"))]
700    #[inline]
701    fn compute_ttfb(&self, _send_ts: u64) -> Option<u64> {
702        None
703    }
704
705    /// Number of in-flight requests.
706    pub fn pending_count(&self) -> usize {
707        self.pending.len()
708    }
709
710    /// Flush before appending the next op if adding it would exceed
711    /// guard/iovec limits for scatter-gather sends. Also enforces the
712    /// `max_in_flight` cap so adversarial / stalled callers can't grow
713    /// the pending queue without bound.
714    fn pre_flush_if_needed(&mut self, has_guard: bool) -> Result<(), Error> {
715        if self.pending.len() >= self.max_in_flight {
716            return Err(Error::TooManyInFlight);
717        }
718        if self.buffered_ops == 0 {
719            return Ok(());
720        }
721        let next_guards = self.write_guards.len() + usize::from(has_guard);
722        let next_parts = 2 * next_guards + 1;
723        if next_guards > MAX_FLUSH_GUARDS || next_parts > MAX_FLUSH_IOVECS {
724            self.flush()?;
725        }
726        Ok(())
727    }
728
729    /// Flush after appending an op if we have reached `max_batch_size`.
730    fn post_flush_if_needed(&mut self) -> Result<(), Error> {
731        if self.buffered_ops >= self.max_batch_size {
732            self.flush()?;
733        }
734        Ok(())
735    }
736
737    /// Flush buffered `fire_*` commands as a single send.
738    ///
739    /// Called automatically by [`recv()`](Self::recv). Call explicitly if you
740    /// need commands to hit the wire before reading responses (e.g., when
741    /// interleaving fire/recv across multiple clients).
742    pub fn flush(&mut self) -> Result<(), Error> {
743        if self.write_buf.is_empty() && self.write_guards.is_empty() {
744            self.buffered_ops = 0;
745            return Ok(());
746        }
747
748        // Send the batch. If the send itself errors (peer RST, kernel
749        // ENOBUFS, etc.), discard the bytes-in-progress and the pending
750        // ops we'd push_back'd for them. Otherwise the next `flush()` would
751        // re-send the same buffer, and `recv()` would pop a `PendingOp` for
752        // a request that was never on the wire and then hang forever
753        // awaiting a response that will never come.
754        let send_outcome: Result<(), Error> = if self.write_guards.is_empty() {
755            self.conn
756                .send_nowait(&self.write_buf)
757                .map(|_| ())
758                .map_err(Error::from)
759        } else {
760            use ringline::SendPart;
761            let mut parts: Vec<SendPart<'_>> = Vec::with_capacity(2 * MAX_FLUSH_GUARDS + 1);
762            let mut pos = 0;
763            for (offset, guard) in self.write_guards.drain(..) {
764                if offset > pos {
765                    parts.push(SendPart::Copy(&self.write_buf[pos..offset]));
766                }
767                parts.push(SendPart::Guard(guard));
768                pos = offset;
769            }
770            if pos < self.write_buf.len() {
771                parts.push(SendPart::Copy(&self.write_buf[pos..]));
772            }
773            self.conn
774                .send_parts()
775                .submit_batch(parts)
776                .map(|_| ())
777                .map_err(Error::from)
778        };
779
780        if let Err(e) = send_outcome {
781            // Discard everything that was buffered for this flush. The
782            // pending ops that haven't been finalised yet (i.e., those
783            // beyond `flushed_count`) correspond to commands that were
784            // pushed onto `pending` but never reached the wire — drop
785            // them so `recv()` doesn't try to read responses for them.
786            self.write_buf.clear();
787            self.write_guards.clear();
788            self.buffered_ops = 0;
789            self.pending.truncate(self.flushed_count);
790            return Err(e);
791        }
792
793        // Only rewrite send timestamps when batching multiple ops —
794        // for a single buffered op the timestamp captured at fire time
795        // is already accurate.
796        if self.buffered_ops >= 1 {
797            let (send_ts, start) = self.timing_start();
798            for pending in self.pending.iter_mut().skip(self.flushed_count) {
799                pending.send_ts = send_ts;
800                pending.start = start;
801            }
802        }
803        self.flushed_count = self.pending.len();
804
805        self.write_buf.clear();
806        self.write_guards.clear();
807        self.buffered_ops = 0;
808
809        Ok(())
810    }
811
812    /// Fire a GET request without waiting for the response.
813    pub fn fire_get(&mut self, key: &[u8], user_data: u64) -> Result<(), Error> {
814        self.pre_flush_if_needed(false)?;
815        let tx_bytes;
816        if self.max_batch_size == 1 && self.write_guards.is_empty() && self.write_buf.is_empty() {
817            // Direct send — skip write_buf round-trip.
818            self.encode_buf.clear();
819            encode_request_into(&McRequest::get(key), &mut self.encode_buf)?;
820            tx_bytes = self.encode_buf.len() as u32;
821            self.conn.send_nowait(&self.encode_buf)?;
822        } else {
823            let before = self.write_buf.len();
824            encode_request_into(&McRequest::get(key), &mut self.write_buf)?;
825            tx_bytes = (self.write_buf.len() - before) as u32;
826            self.buffered_ops += 1;
827        }
828        let (send_ts, start) = self.timing_start_buffered();
829        self.pending.push_back(PendingOp {
830            kind: PendingOpKind::Get,
831            send_ts,
832            start,
833            user_data,
834            tx_bytes,
835        });
836        // Direct sends are already on the wire: keep flushed_count in
837        // sync so a later flush() failure only truncates pending ops
838        // that were never sent. Without this, the error path dropped
839        // an op whose response WILL arrive — permanently misattributing
840        // every subsequent response.
841        if self.max_batch_size == 1 && self.write_guards.is_empty() && self.write_buf.is_empty() {
842            self.flushed_count += 1;
843        }
844        self.post_flush_if_needed()?;
845        Ok(())
846    }
847
848    /// Fire a SET request (with copy) without waiting for the response.
849    pub fn fire_set(
850        &mut self,
851        key: &[u8],
852        value: &[u8],
853        flags: u32,
854        exptime: u32,
855        user_data: u64,
856    ) -> Result<(), Error> {
857        self.pre_flush_if_needed(false)?;
858        let req = McRequest::Set {
859            key,
860            value,
861            flags,
862            exptime,
863        };
864        let tx_bytes;
865        if self.max_batch_size == 1 && self.write_guards.is_empty() && self.write_buf.is_empty() {
866            // Direct send — skip write_buf round-trip.
867            self.encode_buf.clear();
868            encode_request_into(&req, &mut self.encode_buf)?;
869            tx_bytes = self.encode_buf.len() as u32;
870            self.conn.send_nowait(&self.encode_buf)?;
871        } else {
872            let before = self.write_buf.len();
873            encode_request_into(&req, &mut self.write_buf)?;
874            tx_bytes = (self.write_buf.len() - before) as u32;
875            self.buffered_ops += 1;
876        }
877        let (send_ts, start) = self.timing_start_buffered();
878        self.pending.push_back(PendingOp {
879            kind: PendingOpKind::Set,
880            send_ts,
881            start,
882            user_data,
883            tx_bytes,
884        });
885        // Direct sends are already on the wire: keep flushed_count in
886        // sync so a later flush() failure only truncates pending ops
887        // that were never sent. Without this, the error path dropped
888        // an op whose response WILL arrive — permanently misattributing
889        // every subsequent response.
890        if self.max_batch_size == 1 && self.write_guards.is_empty() && self.write_buf.is_empty() {
891            self.flushed_count += 1;
892        }
893        self.post_flush_if_needed()?;
894        Ok(())
895    }
896
897    /// Fire a SET request with zero-copy value via SendGuard.
898    ///
899    /// The guard value is kept alive and sent zero-copy at flush time via
900    /// scatter-gather I/O. The command prefix/suffix are buffered as copy data.
901    pub fn fire_set_with_guard<G: SendGuard>(
902        &mut self,
903        key: &[u8],
904        guard: G,
905        flags: u32,
906        exptime: u32,
907        user_data: u64,
908    ) -> Result<(), Error> {
909        let (ptr, value_len) = guard.as_ptr_len();
910        // Small guard values: fold into the coalescing copy path (`fire_set`)
911        // so they batch like plain SETs instead of taking the scatter-gather
912        // guard path, which flushes every few ops.
913        if self.zc_threshold != 0 && value_len < self.zc_threshold {
914            // SAFETY: `guard` is owned and live for the duration of this
915            // function, so `ptr`/`value_len` describe a valid byte range.
916            // `fire_set` copies those bytes into `write_buf`/`encode_buf`
917            // synchronously (no `.await` between this borrow and the copy),
918            // after which the guard `G` is dropped at the end of this call.
919            let value = unsafe { core::slice::from_raw_parts(ptr, value_len as usize) };
920            return self.fire_set(key, value, flags, exptime, user_data);
921        }
922        self.pre_flush_if_needed(true)?;
923        // Append prefix, record guard insertion point, append suffix —
924        // directly into write_buf, no intermediate allocation.
925        let before = self.write_buf.len();
926        append_set_guard_prefix(&mut self.write_buf, key, value_len as usize, flags, exptime)?;
927        self.write_guards
928            .push((self.write_buf.len(), GuardBox::new(guard)));
929        self.write_buf.extend_from_slice(b"\r\n");
930        let tx_bytes = (self.write_buf.len() - before + value_len as usize) as u32;
931        self.buffered_ops += 1;
932        let (send_ts, start) = self.timing_start_buffered();
933        self.pending.push_back(PendingOp {
934            kind: PendingOpKind::Set,
935            send_ts,
936            start,
937            user_data,
938            tx_bytes,
939        });
940        self.post_flush_if_needed()?;
941        Ok(())
942    }
943
944    /// Fire a DELETE request without waiting for the response.
945    pub fn fire_delete(&mut self, key: &[u8], user_data: u64) -> Result<(), Error> {
946        self.pre_flush_if_needed(false)?;
947        let tx_bytes;
948        if self.max_batch_size == 1 && self.write_guards.is_empty() && self.write_buf.is_empty() {
949            // Direct send — skip write_buf round-trip.
950            self.encode_buf.clear();
951            encode_request_into(&McRequest::delete(key), &mut self.encode_buf)?;
952            tx_bytes = self.encode_buf.len() as u32;
953            self.conn.send_nowait(&self.encode_buf)?;
954        } else {
955            let before = self.write_buf.len();
956            encode_request_into(&McRequest::delete(key), &mut self.write_buf)?;
957            tx_bytes = (self.write_buf.len() - before) as u32;
958            self.buffered_ops += 1;
959        }
960        let (send_ts, start) = self.timing_start_buffered();
961        self.pending.push_back(PendingOp {
962            kind: PendingOpKind::Delete,
963            send_ts,
964            start,
965            user_data,
966            tx_bytes,
967        });
968        // Direct sends are already on the wire: keep flushed_count in
969        // sync so a later flush() failure only truncates pending ops
970        // that were never sent. Without this, the error path dropped
971        // an op whose response WILL arrive — permanently misattributing
972        // every subsequent response.
973        if self.max_batch_size == 1 && self.write_guards.is_empty() && self.write_buf.is_empty() {
974            self.flushed_count += 1;
975        }
976        self.post_flush_if_needed()?;
977        Ok(())
978    }
979
980    /// Receive the next completed operation from the pipeline.
981    ///
982    /// Returns `Err(Error::NoPending)` if there are no in-flight requests.
983    pub async fn recv(&mut self) -> Result<CompletedOp, Error> {
984        // Flush any buffered fire_* commands before reading.
985        self.flush()?;
986
987        let pending = self.pending.pop_front().ok_or(Error::NoPending)?;
988        self.flushed_count = self.flushed_count.saturating_sub(1);
989
990        let response = match self.read_response().await {
991            Ok(v) => v,
992            Err(e) => {
993                // Connection is broken — clear remaining pending ops so
994                // subsequent recv() calls return NoPending instead of
995                // reading stale/misaligned responses. Reset `flushed_count`
996                // too — otherwise a stale count > 0 makes the next batched
997                // `flush()` skip the send_ts rewrite for newly-buffered ops.
998                self.pending.clear();
999                self.flushed_count = 0;
1000                return Err(e);
1001            }
1002        };
1003        // TTFB from the kernel timestamp of the data that completed THIS
1004        // read. Sampling before the read used the PREVIOUS response's
1005        // arrival time for every op after the first in a pipelined batch.
1006        let ttfb_ns = self.compute_ttfb(pending.send_ts);
1007        let latency_ns = match pending.start {
1008            Some(start) => self.finish_timing(pending.send_ts, start),
1009            None => 0,
1010        };
1011        let rx_bytes = self.last_rx_bytes.get();
1012        let tx_bytes = pending.tx_bytes;
1013
1014        let op = match pending.kind {
1015            PendingOpKind::Get => {
1016                // Check for error responses first
1017                let result = match check_error_bytes(&response) {
1018                    Err(e) => Err(e),
1019                    Ok(()) => match response {
1020                        McResponseBytes::Values(mut values) => {
1021                            if values.is_empty() {
1022                                Ok(None)
1023                            } else {
1024                                let v = values.swap_remove(0);
1025                                Ok(Some(Value {
1026                                    data: v.data,
1027                                    flags: v.flags,
1028                                }))
1029                            }
1030                        }
1031                        _ => Err(Error::UnexpectedResponse),
1032                    },
1033                };
1034                let (success, hit) = match &result {
1035                    Ok(Some(_)) => (true, Some(true)),
1036                    Ok(None) => (true, Some(false)),
1037                    Err(_) => (false, None),
1038                };
1039                self.record(&CommandResult {
1040                    command: CommandType::Get,
1041                    latency_ns,
1042                    hit,
1043                    success,
1044                    ttfb_ns,
1045                    tx_bytes,
1046                    rx_bytes,
1047                });
1048                CompletedOp::Get {
1049                    result,
1050                    user_data: pending.user_data,
1051                    latency_ns,
1052                }
1053            }
1054            PendingOpKind::Set => {
1055                let result = match check_error_bytes(&response) {
1056                    Err(e) => Err(e),
1057                    Ok(()) => match response {
1058                        McResponseBytes::Stored => Ok(()),
1059                        _ => Err(Error::UnexpectedResponse),
1060                    },
1061                };
1062                self.record(&CommandResult {
1063                    command: CommandType::Set,
1064                    latency_ns,
1065                    hit: None,
1066                    success: result.is_ok(),
1067                    ttfb_ns,
1068                    tx_bytes,
1069                    rx_bytes,
1070                });
1071                CompletedOp::Set {
1072                    result,
1073                    user_data: pending.user_data,
1074                    latency_ns,
1075                }
1076            }
1077            PendingOpKind::Delete => {
1078                let result = match check_error_bytes(&response) {
1079                    Err(e) => Err(e),
1080                    Ok(()) => match response {
1081                        McResponseBytes::Deleted => Ok(true),
1082                        McResponseBytes::NotFound => Ok(false),
1083                        _ => Err(Error::UnexpectedResponse),
1084                    },
1085                };
1086                self.record(&CommandResult {
1087                    command: CommandType::Delete,
1088                    latency_ns,
1089                    hit: None,
1090                    success: result.is_ok(),
1091                    ttfb_ns,
1092                    tx_bytes,
1093                    rx_bytes,
1094                });
1095                CompletedOp::Delete {
1096                    result,
1097                    user_data: pending.user_data,
1098                    latency_ns,
1099                }
1100            }
1101        };
1102
1103        Ok(op)
1104    }
1105
1106    // ── Internal I/O (unchanged) ────────────────────────────────────────
1107
1108    /// Read and parse a single Memcache response from the connection.
1109    ///
1110    /// Uses zero-copy parsing via `with_bytes` + `ResponseBytes::parse`:
1111    /// value data are `Bytes::slice()` references into the accumulator's
1112    /// buffer rather than freshly allocated `Vec<u8>`.
1113    ///
1114    /// On `Error::Protocol` (parse failure) the underlying connection is
1115    /// closed: even though the parser advanced past the malformed bytes,
1116    /// the request/response framing is now irrecoverably misaligned and
1117    /// any further command would read garbage. Surfacing `Protocol` as a
1118    /// terminal error matches the recv() pending-queue clear and avoids
1119    /// silently desynced clients.
1120    pub(crate) async fn read_response(&self) -> Result<McResponseBytes, Error> {
1121        let mut result: Option<Result<McResponseBytes, Error>> = None;
1122        let n = self
1123            .conn
1124            .with_bytes(|bytes| {
1125                let len = bytes.len();
1126                match McResponseBytes::parse(bytes) {
1127                    Ok((response, consumed)) => {
1128                        result = Some(Ok(response));
1129                        ParseResult::Consumed(consumed)
1130                    }
1131                    Err(e) if e.is_incomplete() => ParseResult::Consumed(0),
1132                    Err(e) => {
1133                        result = Some(Err(Error::Protocol(e)));
1134                        ParseResult::Consumed(len)
1135                    }
1136                }
1137            })
1138            .await;
1139        self.last_rx_bytes.set(n as u32);
1140        if n == 0 {
1141            return result.unwrap_or(Err(Error::ConnectionClosed));
1142        }
1143        let r = result.unwrap();
1144        if matches!(r, Err(Error::Protocol(_))) {
1145            self.conn.close();
1146        }
1147        r
1148    }
1149
1150    /// Send an encoded command and read the response, converting error
1151    /// responses into `Error::Memcache`.
1152    async fn execute(&self, encoded: &[u8]) -> Result<McResponseBytes, Error> {
1153        self.conn.send(encoded)?;
1154        let response = self.read_response().await?;
1155        check_error_bytes(&response)?;
1156        Ok(response)
1157    }
1158
1159    // -- Commands (instrumented hot-path) ---------------------------------
1160
1161    /// Get the value of a key. Returns `None` on cache miss.
1162    pub async fn get(&mut self, key: impl AsRef<[u8]>) -> Result<Option<Value>, Error> {
1163        let key = key.as_ref();
1164        self.encode_buf.clear();
1165        encode_request_into(&McRequest::get(key), &mut self.encode_buf)?;
1166
1167        if !self.is_instrumented() {
1168            let response = self.execute(&self.encode_buf).await?;
1169            return match response {
1170                McResponseBytes::Values(mut values) => {
1171                    if values.is_empty() {
1172                        Ok(None)
1173                    } else {
1174                        let v = values.swap_remove(0);
1175                        Ok(Some(Value {
1176                            data: v.data,
1177                            flags: v.flags,
1178                        }))
1179                    }
1180                }
1181                _ => Err(Error::UnexpectedResponse),
1182            };
1183        }
1184
1185        let tx_bytes = self.encode_buf.len() as u32;
1186        let send_ts = self.send_timestamp();
1187        let start = Instant::now();
1188        let response = self.execute(&self.encode_buf).await;
1189        let latency_ns = self.finish_timing(send_ts, start);
1190        let rx_bytes = self.last_rx_bytes.get();
1191
1192        let result = match response {
1193            Ok(McResponseBytes::Values(mut values)) => {
1194                if values.is_empty() {
1195                    Ok(None)
1196                } else {
1197                    let v = values.swap_remove(0);
1198                    Ok(Some(Value {
1199                        data: v.data,
1200                        flags: v.flags,
1201                    }))
1202                }
1203            }
1204            Ok(_) => Err(Error::UnexpectedResponse),
1205            Err(e) => Err(e),
1206        };
1207
1208        let (success, hit) = match &result {
1209            Ok(Some(_)) => (true, Some(true)),
1210            Ok(None) => (true, Some(false)),
1211            Err(_) => (false, None),
1212        };
1213        self.record(&CommandResult {
1214            command: CommandType::Get,
1215            latency_ns,
1216            hit,
1217            success,
1218            ttfb_ns: None,
1219            tx_bytes,
1220            rx_bytes,
1221        });
1222        result
1223    }
1224
1225    /// Get values for multiple keys. Returns only hits, each with its key and CAS token.
1226    pub async fn gets(&mut self, keys: &[&[u8]]) -> Result<Vec<GetValue>, Error> {
1227        if keys.is_empty() {
1228            return Ok(Vec::new());
1229        }
1230        let encoded = encode_request(&McRequest::gets(keys))?;
1231        let response = self.execute(&encoded).await?;
1232        match response {
1233            McResponseBytes::Values(values) => Ok(values
1234                .into_iter()
1235                .map(|v| GetValue {
1236                    key: v.key,
1237                    data: v.data,
1238                    flags: v.flags,
1239                    cas: v.cas,
1240                })
1241                .collect()),
1242            _ => Err(Error::UnexpectedResponse),
1243        }
1244    }
1245
1246    /// Set a key-value pair with default flags (0) and no expiration.
1247    pub async fn set(
1248        &mut self,
1249        key: impl AsRef<[u8]>,
1250        value: impl AsRef<[u8]>,
1251    ) -> Result<(), Error> {
1252        self.set_with_options(key, value, 0, 0).await
1253    }
1254
1255    /// Set a key-value pair with custom flags and expiration time.
1256    pub async fn set_with_options(
1257        &mut self,
1258        key: impl AsRef<[u8]>,
1259        value: impl AsRef<[u8]>,
1260        flags: u32,
1261        exptime: u32,
1262    ) -> Result<(), Error> {
1263        let key = key.as_ref();
1264        let value = value.as_ref();
1265        self.encode_buf.clear();
1266        encode_request_into(
1267            &McRequest::Set {
1268                key,
1269                value,
1270                flags,
1271                exptime,
1272            },
1273            &mut self.encode_buf,
1274        )?;
1275
1276        if !self.is_instrumented() {
1277            let response = self.execute(&self.encode_buf).await?;
1278            return match response {
1279                McResponseBytes::Stored => Ok(()),
1280                _ => Err(Error::UnexpectedResponse),
1281            };
1282        }
1283
1284        let tx_bytes = self.encode_buf.len() as u32;
1285        let send_ts = self.send_timestamp();
1286        let start = Instant::now();
1287        let response = self.execute(&self.encode_buf).await;
1288        let latency_ns = self.finish_timing(send_ts, start);
1289        let rx_bytes = self.last_rx_bytes.get();
1290
1291        let result = match response {
1292            Ok(McResponseBytes::Stored) => Ok(()),
1293            Ok(_) => Err(Error::UnexpectedResponse),
1294            Err(e) => Err(e),
1295        };
1296        self.record(&CommandResult {
1297            command: CommandType::Set,
1298            latency_ns,
1299            hit: None,
1300            success: result.is_ok(),
1301            ttfb_ns: None,
1302            tx_bytes,
1303            rx_bytes,
1304        });
1305        result
1306    }
1307
1308    /// Store a key only if it does not already exist (ADD command).
1309    /// Returns `true` if stored, `false` if the key already exists.
1310    pub async fn add(
1311        &mut self,
1312        key: impl AsRef<[u8]>,
1313        value: impl AsRef<[u8]>,
1314    ) -> Result<bool, Error> {
1315        let key = key.as_ref();
1316        let value = value.as_ref();
1317        let encoded = encode_add(key, value)?;
1318        let response = self.execute(&encoded).await?;
1319        match response {
1320            McResponseBytes::Stored => Ok(true),
1321            McResponseBytes::NotStored => Ok(false),
1322            _ => Err(Error::UnexpectedResponse),
1323        }
1324    }
1325
1326    /// Store a key only if it already exists (REPLACE command).
1327    /// Returns `true` if stored, `false` if the key does not exist.
1328    pub async fn replace(
1329        &mut self,
1330        key: impl AsRef<[u8]>,
1331        value: impl AsRef<[u8]>,
1332    ) -> Result<bool, Error> {
1333        let key = key.as_ref();
1334        let value = value.as_ref();
1335        let encoded = encode_request(&McRequest::Replace {
1336            key,
1337            value,
1338            flags: 0,
1339            exptime: 0,
1340        })?;
1341        let response = self.execute(&encoded).await?;
1342        match response {
1343            McResponseBytes::Stored => Ok(true),
1344            McResponseBytes::NotStored => Ok(false),
1345            _ => Err(Error::UnexpectedResponse),
1346        }
1347    }
1348
1349    /// Increment a numeric value by delta. Returns the new value after incrementing.
1350    /// Returns `None` if the key does not exist.
1351    pub async fn incr(&mut self, key: impl AsRef<[u8]>, delta: u64) -> Result<Option<u64>, Error> {
1352        let key = key.as_ref();
1353        let encoded = encode_request(&McRequest::incr(key, delta))?;
1354        let response = self.execute(&encoded).await?;
1355        match response {
1356            McResponseBytes::Numeric(val) => Ok(Some(val)),
1357            McResponseBytes::NotFound => Ok(None),
1358            _ => Err(Error::UnexpectedResponse),
1359        }
1360    }
1361
1362    /// Decrement a numeric value by delta. Returns the new value after decrementing.
1363    /// Returns `None` if the key does not exist.
1364    pub async fn decr(&mut self, key: impl AsRef<[u8]>, delta: u64) -> Result<Option<u64>, Error> {
1365        let key = key.as_ref();
1366        let encoded = encode_request(&McRequest::decr(key, delta))?;
1367        let response = self.execute(&encoded).await?;
1368        match response {
1369            McResponseBytes::Numeric(val) => Ok(Some(val)),
1370            McResponseBytes::NotFound => Ok(None),
1371            _ => Err(Error::UnexpectedResponse),
1372        }
1373    }
1374
1375    /// Append data to an existing item's value.
1376    /// Returns `true` if stored, `false` if the key does not exist.
1377    pub async fn append(
1378        &mut self,
1379        key: impl AsRef<[u8]>,
1380        value: impl AsRef<[u8]>,
1381    ) -> Result<bool, Error> {
1382        let key = key.as_ref();
1383        let value = value.as_ref();
1384        let encoded = encode_request(&McRequest::append(key, value))?;
1385        let response = self.execute(&encoded).await?;
1386        match response {
1387            McResponseBytes::Stored => Ok(true),
1388            McResponseBytes::NotStored => Ok(false),
1389            _ => Err(Error::UnexpectedResponse),
1390        }
1391    }
1392
1393    /// Prepend data to an existing item's value.
1394    /// Returns `true` if stored, `false` if the key does not exist.
1395    pub async fn prepend(
1396        &mut self,
1397        key: impl AsRef<[u8]>,
1398        value: impl AsRef<[u8]>,
1399    ) -> Result<bool, Error> {
1400        let key = key.as_ref();
1401        let value = value.as_ref();
1402        let encoded = encode_request(&McRequest::prepend(key, value))?;
1403        let response = self.execute(&encoded).await?;
1404        match response {
1405            McResponseBytes::Stored => Ok(true),
1406            McResponseBytes::NotStored => Ok(false),
1407            _ => Err(Error::UnexpectedResponse),
1408        }
1409    }
1410
1411    /// Compare-and-swap: store the value only if the CAS token matches.
1412    /// Returns `Ok(true)` if stored, `Ok(false)` if the CAS token didn't match (EXISTS),
1413    /// or `Err` if the key was not found or another error occurred.
1414    pub async fn cas(
1415        &mut self,
1416        key: impl AsRef<[u8]>,
1417        value: impl AsRef<[u8]>,
1418        cas_unique: u64,
1419    ) -> Result<bool, Error> {
1420        let key = key.as_ref();
1421        let value = value.as_ref();
1422        let encoded = encode_request(&McRequest::cas(key, value, cas_unique))?;
1423        let response = self.execute(&encoded).await?;
1424        match response {
1425            McResponseBytes::Stored => Ok(true),
1426            McResponseBytes::Exists => Ok(false),
1427            McResponseBytes::NotFound => Err(Error::Memcache("NOT_FOUND".into())),
1428            _ => Err(Error::UnexpectedResponse),
1429        }
1430    }
1431
1432    /// Delete a key. Returns `true` if deleted, `false` if not found.
1433    pub async fn delete(&mut self, key: impl AsRef<[u8]>) -> Result<bool, Error> {
1434        let key = key.as_ref();
1435        self.encode_buf.clear();
1436        encode_request_into(&McRequest::delete(key), &mut self.encode_buf)?;
1437
1438        if !self.is_instrumented() {
1439            let response = self.execute(&self.encode_buf).await?;
1440            return match response {
1441                McResponseBytes::Deleted => Ok(true),
1442                McResponseBytes::NotFound => Ok(false),
1443                _ => Err(Error::UnexpectedResponse),
1444            };
1445        }
1446
1447        let tx_bytes = self.encode_buf.len() as u32;
1448        let send_ts = self.send_timestamp();
1449        let start = Instant::now();
1450        let response = self.execute(&self.encode_buf).await;
1451        let latency_ns = self.finish_timing(send_ts, start);
1452        let rx_bytes = self.last_rx_bytes.get();
1453
1454        let result = match response {
1455            Ok(McResponseBytes::Deleted) => Ok(true),
1456            Ok(McResponseBytes::NotFound) => Ok(false),
1457            Ok(_) => Err(Error::UnexpectedResponse),
1458            Err(e) => Err(e),
1459        };
1460        self.record(&CommandResult {
1461            command: CommandType::Delete,
1462            latency_ns,
1463            hit: None,
1464            success: result.is_ok(),
1465            ttfb_ns: None,
1466            tx_bytes,
1467            rx_bytes,
1468        });
1469        result
1470    }
1471
1472    /// Flush all items from the cache.
1473    pub async fn flush_all(&mut self) -> Result<(), Error> {
1474        let encoded = encode_request(&McRequest::flush_all())?;
1475        let response = self.execute(&encoded).await?;
1476        match response {
1477            McResponseBytes::Ok => Ok(()),
1478            _ => Err(Error::UnexpectedResponse),
1479        }
1480    }
1481
1482    /// Get the server version string.
1483    pub async fn version(&mut self) -> Result<Box<str>, Error> {
1484        let encoded = encode_request(&McRequest::version())?;
1485        let response = self.execute(&encoded).await?;
1486        match response {
1487            McResponseBytes::Version(v) => Ok(Box::from(String::from_utf8_lossy(v.as_ref()))),
1488            _ => Err(Error::UnexpectedResponse),
1489        }
1490    }
1491
1492    // -- Zero-copy SET -------------------------------------------------------
1493
1494    /// SET with zero-copy value via SendGuard. The guard pins value memory
1495    /// until the kernel completes the send.
1496    pub async fn set_with_guard<G: SendGuard>(
1497        &mut self,
1498        key: &[u8],
1499        guard: G,
1500        flags: u32,
1501        exptime: u32,
1502    ) -> Result<(), Error> {
1503        if !self.is_instrumented() {
1504            let (_, value_len) = guard.as_ptr_len();
1505            self.encode_buf.clear();
1506            append_set_guard_prefix(
1507                &mut self.encode_buf,
1508                key,
1509                value_len as usize,
1510                flags,
1511                exptime,
1512            )?;
1513
1514            let prefix: &[u8] = &self.encode_buf;
1515            self.conn.send_parts().build(move |b| {
1516                b.copy(prefix)
1517                    .guard(GuardBox::new(guard))
1518                    .copy(b"\r\n")
1519                    .submit()
1520            })?;
1521
1522            let response = self.read_response().await?;
1523            check_error_bytes(&response)?;
1524            return match response {
1525                McResponseBytes::Stored => Ok(()),
1526                _ => Err(Error::UnexpectedResponse),
1527            };
1528        }
1529
1530        let (_, value_len) = guard.as_ptr_len();
1531        self.encode_buf.clear();
1532        append_set_guard_prefix(
1533            &mut self.encode_buf,
1534            key,
1535            value_len as usize,
1536            flags,
1537            exptime,
1538        )?;
1539        let tx_bytes = (self.encode_buf.len() + value_len as usize + 2) as u32;
1540
1541        let send_ts = self.send_timestamp();
1542        let start = Instant::now();
1543
1544        let prefix: &[u8] = &self.encode_buf;
1545        self.conn.send_parts().build(move |b| {
1546            b.copy(prefix)
1547                .guard(GuardBox::new(guard))
1548                .copy(b"\r\n")
1549                .submit()
1550        })?;
1551
1552        let response = self.read_response().await;
1553        let latency_ns = self.finish_timing(send_ts, start);
1554        let rx_bytes = self.last_rx_bytes.get();
1555
1556        let result = match response {
1557            Ok(ref r) => {
1558                check_error_bytes(r)?;
1559                match r {
1560                    McResponseBytes::Stored => Ok(()),
1561                    _ => Err(Error::UnexpectedResponse),
1562                }
1563            }
1564            Err(e) => Err(e),
1565        };
1566        self.record(&CommandResult {
1567            command: CommandType::Set,
1568            latency_ns,
1569            hit: None,
1570            success: result.is_ok(),
1571            ttfb_ns: None,
1572            tx_bytes,
1573            rx_bytes,
1574        });
1575        result
1576    }
1577}
1578
1579// ── Streaming SET (send-side, single-connection Client only) ──────────────
1580//
1581// `set_stream` writes a large value from a caller-provided [`SegmentSource`]
1582// without gathering it into one contiguous buffer: the command header (which
1583// declares the length) is sent, then value chunks are pulled from the source and
1584// streamed to the wire, then the trailing `\r\n`. Works on both backends (send
1585// works everywhere); v1 copies each chunk into the send pool (`send`) — zero-copy
1586// guarded streaming is a later optimization.
1587
1588/// A source of exactly `len` value bytes for a streaming
1589/// [`set_stream`](Client::set_stream), yielded in chunks.
1590///
1591/// The client pulls chunks with [`next_chunk`](SegmentSource::next_chunk) until
1592/// it returns `Ok(None)` (exhausted), counting bytes as it goes. If the total
1593/// number of bytes yielded differs from the `len` passed to `set_stream`, the
1594/// send is a protocol desync and the connection is closed
1595/// ([`Error::LengthMismatch`]).
1596///
1597/// `next_chunk` is **synchronous** by design: it avoids the `async_fn_in_trait`
1598/// public-API lint and keeps v1 simple. A source backed by async I/O should
1599/// pre-buffer each chunk before yielding it. (An async pull is a documented
1600/// follow-up.)
1601pub trait SegmentSource {
1602    /// Yield the next chunk of value bytes, or `Ok(None)` once exhausted. An
1603    /// empty chunk is skipped (treated as "no bytes this call", not end).
1604    fn next_chunk(&mut self) -> Result<Option<Bytes>, Error>;
1605}
1606
1607/// A [`SegmentSource`] that yields chunks from an in-memory iterator of
1608/// [`Bytes`]. Convenience for callers that already have their value in chunks
1609/// (or a single chunk); the streaming win is that ringline never gathers them
1610/// into one contiguous buffer.
1611impl<I: Iterator<Item = Bytes>> SegmentSource for I {
1612    fn next_chunk(&mut self) -> Result<Option<Bytes>, Error> {
1613        Ok(self.next())
1614    }
1615}
1616
1617impl Client {
1618    /// Streaming SET (single-connection [`Client`] only).
1619    ///
1620    /// Sends `set <key> <flags> <exptime> <len>\r\n`, then streams exactly `len`
1621    /// value bytes pulled from `src`, then the trailing `\r\n`, and reads the
1622    /// `STORED\r\n` reply. The value is never gathered into one contiguous buffer
1623    /// on the client side.
1624    ///
1625    /// # Length contract
1626    ///
1627    /// `len` is authoritative: it is written into the command header. The client
1628    /// counts the bytes pulled from `src` and returns [`Error::LengthMismatch`]
1629    /// (after closing the connection) if `src` yields fewer or more than `len`
1630    /// bytes — because a partial/oversized value frame is already on the wire and
1631    /// the connection is irrecoverably desynced (same discipline as the read-side
1632    /// poison rule).
1633    ///
1634    /// # Backends
1635    ///
1636    /// Works on both io_uring and mio. v1 copies each chunk into the send pool
1637    /// via [`ConnCtx::send`](ringline::ConnCtx::send) (awaited per chunk for
1638    /// backpressure). Zero-copy guarded streaming is a later optimization.
1639    pub async fn set_stream(
1640        &mut self,
1641        key: &[u8],
1642        flags: u32,
1643        exptime: u32,
1644        len: usize,
1645        src: impl SegmentSource,
1646    ) -> Result<(), Error> {
1647        // Header: `set <key> <flags> <exptime> <len>\r\n`. Build and validate it
1648        // first — an oversized key is rejected here, before any byte hits the
1649        // wire, so that error does NOT poison the connection.
1650        self.encode_buf.clear();
1651        append_set_guard_prefix(&mut self.encode_buf, key, len, flags, exptime)?;
1652
1653        // From the first send onward a partial value frame can be on the wire;
1654        // any error before a *complete* reply is read leaves the connection
1655        // desynced — a pooled reuse would feed the next caller's bytes into this
1656        // frame's value. So poison (close) on any such error, matching the
1657        // over/under-produce and read-side poison discipline. A complete reply
1658        // (even a server error line, or an unexpected type) means the frame was
1659        // fully sent and the wire is synced — that path must NOT poison.
1660        match self.write_set_frame_and_read(len, src).await {
1661            Ok(response) => {
1662                check_error_bytes(&response)?;
1663                match response {
1664                    McResponseBytes::Stored => Ok(()),
1665                    _ => Err(Error::UnexpectedResponse),
1666                }
1667            }
1668            Err(e) => {
1669                self.conn.close();
1670                Err(e)
1671            }
1672        }
1673    }
1674
1675    /// Write the streamed `set` frame (header already built in `encode_buf`) and
1676    /// read its reply. Every `?` here is a wire-desync condition
1677    /// ([`set_stream`](Self::set_stream) poisons on it); classifying the reply
1678    /// happens in the caller, after a complete reply has been read.
1679    async fn write_set_frame_and_read(
1680        &mut self,
1681        len: usize,
1682        mut src: impl SegmentSource,
1683    ) -> Result<McResponseBytes, Error> {
1684        self.conn.send_nowait(&self.encode_buf)?;
1685
1686        // Stream the value body, enforcing the length contract.
1687        let mut sent = 0usize;
1688        while let Some(chunk) = src.next_chunk()? {
1689            if chunk.is_empty() {
1690                continue;
1691            }
1692            sent += chunk.len();
1693            if sent > len {
1694                // Over-produce: the value frame is already too long → desync.
1695                return Err(Error::LengthMismatch);
1696            }
1697            // `send` copies into the pool synchronously and returns a future that
1698            // resolves when the bytes reach the socket — awaiting bounds in-flight
1699            // sends to one, so a large value can't exhaust the send pool.
1700            self.conn.send(&chunk)?.await?;
1701        }
1702        if sent != len {
1703            // Under-produce: header declared `len`, source gave fewer → desync.
1704            return Err(Error::LengthMismatch);
1705        }
1706
1707        // Trailing `\r\n`, then the `STORED\r\n` reply.
1708        self.conn.send_nowait(b"\r\n")?;
1709        self.read_response().await
1710    }
1711}
1712
1713// ── Streaming GET (segmented recv, single-connection Client only) ─────────
1714//
1715// `get_stream` returns a `StreamValue` instead of a materialized `Value`: the
1716// value body is delivered as segments over the runtime's segmented-recv API
1717// (`ConnCtx::recv_owned_segment`), so a consumer that discards or streams the
1718// value never forces the whole thing into one contiguous allocation. Only
1719// `collect()` materializes.
1720//
1721// io_uring only — segmented recv is backed by the provided-buffer ring, and the
1722// underlying `recv_owned_segment` / `end_segments` methods are `#[cfg(has_io_uring)]`.
1723// This crate's build.rs emits `has_io_uring` in lockstep with `ringline`.
1724//
1725// SCOPE (v1): memcache `get_stream` on the single-connection `Client` only.
1726// Deferred (documented, not implemented here): `get_cas` streaming (the CAS
1727// variant), streaming `set`, and `recv_streaming()` fire/recv integration. The
1728// multi-key `gets(keys)` stays eager/materialized (one frame carries N values),
1729// and pooled / sharded clients intentionally keep the materialized `get`.
1730
1731#[cfg(has_io_uring)]
1732impl Client {
1733    /// Streaming GET (single-connection [`Client`] only).
1734    ///
1735    /// Like [`get`](Self::get), but the value body is delivered as a
1736    /// [`StreamValue`] of segments rather than a single materialized [`Value`].
1737    /// A consumer that only needs to discard or checksum the value pays no
1738    /// gather copy; [`StreamValue::collect`] is the one materialization. The
1739    /// item flags are available via [`StreamValue::flags`].
1740    ///
1741    /// Returns `Ok(None)` for a missing key (bare `END\r\n` reply).
1742    ///
1743    /// # Sequential use only
1744    ///
1745    /// The returned stream borrows `&mut self`, so a second concurrent stream (or
1746    /// any other client call) while it is alive is a compile error. `get_stream`
1747    /// must be called on an *idle* connection — not interleaved with pending
1748    /// `fire_*` operations or a half-read pipeline, whose responses would arrive
1749    /// ahead of the GET reply and desync the segmented read.
1750    ///
1751    /// # Dropping mid-stream poisons the connection
1752    ///
1753    /// If a [`StreamValue`] is dropped before its value (and the trailing
1754    /// `\r\nEND\r\n` delimiter) is fully consumed — via
1755    /// [`collect`](StreamValue::collect), [`discard`](StreamValue::discard), or
1756    /// reading [`next_segment`](StreamValue::next_segment) to the end — the
1757    /// undrained wire bytes cannot be drained synchronously in `Drop`, so the
1758    /// connection is **closed** (its slot generation bumped). The next operation
1759    /// on this client then fails, matching the "poison = close" recovery
1760    /// discipline.
1761    pub async fn get_stream(
1762        &mut self,
1763        key: impl AsRef<[u8]>,
1764    ) -> Result<Option<StreamValue<'_>>, Error> {
1765        let key = key.as_ref();
1766        // Fire the GET, then opt the connection into segmented delivery. The
1767        // reply body then arrives as held segments instead of being gathered
1768        // into the accumulator. `recv_owned_segment()` sets the domain
1769        // synchronously (before we await), so the reply can't be processed under
1770        // the default path first.
1771        self.encode_buf.clear();
1772        encode_request_into(&McRequest::get(key), &mut self.encode_buf)?;
1773        self.conn.send_nowait(&self.encode_buf)?;
1774
1775        // Read the `VALUE <key> <flags> <bytes>\r\n` header line (or `END\r\n`).
1776        // It always fits in the first received buffer; the rare split-across-
1777        // buffers case is handled by accumulating into `acc` and retrying.
1778        //
1779        // Demarcation is exact: `parse_get_header` measures only the header line
1780        // (`header_len`, up to and including its `\r\n`), never the value.
1781        // Everything after `header_len` in the buffer we already pulled is
1782        // value-body bytes, handed to the stream as an O(1) `Bytes::slice` — so
1783        // no value byte is lost or double-read, and the read cursor is a single
1784        // monotonic position across header + body + trailing `\r\nEND\r\n`.
1785        let mut acc: Option<Vec<u8>> = None;
1786        loop {
1787            let seg = match self.conn.recv_owned_segment().await {
1788                Ok(Some(b)) => b,
1789                Ok(None) => {
1790                    // Peer closed before any header arrived.
1791                    let _ = self.conn.end_segments();
1792                    return Err(Error::ConnectionClosed);
1793                }
1794                Err(e) => {
1795                    // Recv I/O error mid-header: the read is broken and the
1796                    // connection is still in the segmented domain. Poison it
1797                    // (close) so it is not reused stuck in that domain / desynced.
1798                    self.conn.close();
1799                    return Err(e.into());
1800                }
1801            };
1802            let combined: Bytes = match acc.take() {
1803                None => seg,
1804                Some(mut a) => {
1805                    a.extend_from_slice(&seg);
1806                    Bytes::from(a)
1807                }
1808            };
1809            match parse_get_header(&combined) {
1810                Ok(Some(GetHeader::Miss)) => {
1811                    // Missing key. `END\r\n` carries no value/trailing; restore
1812                    // the default read path so the next command works.
1813                    self.conn.end_segments()?;
1814                    return Ok(None);
1815                }
1816                Ok(Some(GetHeader::Value {
1817                    flags,
1818                    len,
1819                    header_len,
1820                })) => {
1821                    let leftover = combined.slice(header_len..);
1822                    return Ok(Some(StreamValue::new(self, flags, len, leftover)));
1823                }
1824                Ok(None) => {
1825                    // Header line not yet complete — stash and pull the next buffer.
1826                    acc = Some(combined.to_vec());
1827                }
1828                Err(e) => {
1829                    // Server error line (`ERROR`, `SERVER_ERROR …`,
1830                    // `CLIENT_ERROR …`) or a malformed header. In sequential use
1831                    // the whole reply line is in `combined`; restore the read
1832                    // path and surface the error.
1833                    let _ = self.conn.end_segments();
1834                    return Err(e);
1835                }
1836            }
1837        }
1838    }
1839}
1840
1841/// Parsed shape of a memcache GET reply header line.
1842#[cfg(has_io_uring)]
1843enum GetHeader {
1844    /// Bare `END\r\n` — the key does not exist.
1845    Miss,
1846    /// `VALUE <key> <flags> <bytes>\r\n` — `bytes` value bytes follow, then a
1847    /// trailing `\r\nEND\r\n`. `header_len` is the byte length of the header line
1848    /// itself (through its `\r\n`); `flags` is the stored item flags.
1849    Value {
1850        flags: u32,
1851        len: usize,
1852        header_len: usize,
1853    },
1854}
1855
1856/// Position of the first `\r\n` in `buf` (the index of the `\r`), or `None`.
1857#[cfg(has_io_uring)]
1858fn find_crlf(buf: &[u8]) -> Option<usize> {
1859    buf.windows(2).position(|w| w == b"\r\n")
1860}
1861
1862/// Parse a memcache GET reply header line from the **front** of `buf`, measuring
1863/// only the header line (`VALUE <key> <flags> <bytes>\r\n` / `END\r\n`) — the
1864/// value body and trailing `\r\nEND\r\n` are left for the caller to stream, so
1865/// the header/value split loses no byte.
1866///
1867/// - `Ok(None)` — the header line is not yet fully buffered (need more bytes).
1868/// - `Ok(Some(GetHeader::Miss))` — a bare `END` line (cache miss).
1869/// - `Ok(Some(GetHeader::Value { .. }))` — a `VALUE` header.
1870/// - `Err(Error::Memcache(_))` — a server error line (`ERROR` / `SERVER_ERROR …`
1871///   / `CLIENT_ERROR …`).
1872/// - `Err(Error::UnexpectedResponse)` — an unrecognized line or malformed
1873///   integer field.
1874#[cfg(has_io_uring)]
1875fn parse_get_header(buf: &[u8]) -> Result<Option<GetHeader>, Error> {
1876    // Every valid first line (VALUE / END / error) ends in `\r\n`; without one
1877    // the line is incomplete.
1878    let Some(cr) = find_crlf(buf) else {
1879        return Ok(None);
1880    };
1881    let line = &buf[..cr];
1882    let header_len = cr + 2; // line bytes + "\r\n"
1883
1884    if line == b"END" {
1885        return Ok(Some(GetHeader::Miss));
1886    }
1887    if line.starts_with(b"VALUE ") {
1888        // VALUE <key> <flags> <bytes> [<cas>]
1889        let mut fields = line.split(|&b| b == b' ').filter(|f| !f.is_empty());
1890        let _value_kw = fields.next(); // "VALUE"
1891        let _key = fields.next().ok_or(Error::UnexpectedResponse)?;
1892        let flags_tok = fields.next().ok_or(Error::UnexpectedResponse)?;
1893        let bytes_tok = fields.next().ok_or(Error::UnexpectedResponse)?;
1894        let flags = parse_uint::<u32>(flags_tok)?;
1895        let len = parse_uint::<usize>(bytes_tok)?;
1896        return Ok(Some(GetHeader::Value {
1897            flags,
1898            len,
1899            header_len,
1900        }));
1901    }
1902    // Error lines. `ERROR` is a bare token; `SERVER_ERROR`/`CLIENT_ERROR` carry a
1903    // trailing message.
1904    if line == b"ERROR" || line.starts_with(b"CLIENT_ERROR") || line.starts_with(b"SERVER_ERROR") {
1905        return Err(Error::Memcache(String::from_utf8_lossy(line).into_owned()));
1906    }
1907    Err(Error::UnexpectedResponse)
1908}
1909
1910/// Parse an ASCII unsigned integer field, mapping any malformation to
1911/// [`Error::UnexpectedResponse`].
1912#[cfg(has_io_uring)]
1913fn parse_uint<T: std::str::FromStr>(tok: &[u8]) -> Result<T, Error> {
1914    std::str::from_utf8(tok)
1915        .ok()
1916        .and_then(|s| s.parse::<T>().ok())
1917        .ok_or(Error::UnexpectedResponse)
1918}
1919
1920/// A streaming memcache GET value (see [`Client::get_stream`]).
1921///
1922/// Borrows the [`Client`] exclusively (`&mut`) and is bounded to the value length
1923/// parsed from the `VALUE <key> <flags> <bytes>\r\n` header, so it can neither
1924/// over-read into a following reply nor be used concurrently with another client
1925/// operation.
1926///
1927/// The value body is delivered via the runtime's segmented-recv API. In v1 each
1928/// [`next_segment`](Self::next_segment) chunk (and the [`collect`](Self::collect)
1929/// gather) is an owned [`Bytes`] copied at delivery (Mode C). [`discard`](Self::discard)
1930/// consumes each chunk at delivery but performs no *gather* copy.
1931#[cfg(has_io_uring)]
1932pub struct StreamValue<'a> {
1933    client: &'a mut Client,
1934    /// Item flags from the `VALUE` header.
1935    flags: u32,
1936    /// Full value length from the header.
1937    len: usize,
1938    /// Value bytes not yet handed to the caller.
1939    value_left: usize,
1940    /// Trailing `\r\nEND\r\n` bytes not yet consumed (starts at 7).
1941    trail_left: usize,
1942    /// Bytes pulled from the wire but not yet consumed (value + maybe trailing).
1943    buf: Bytes,
1944    /// Set once value + trailing delimiter are fully consumed and the recv
1945    /// domain has been restored. `false` at drop time ⇒ the stream was abandoned
1946    /// mid-value ⇒ poison (close) the connection.
1947    finished: bool,
1948}
1949
1950/// Length of the trailing `\r\nEND\r\n` after a memcache value body: the value's
1951/// own CRLF (2) plus the `END\r\n` terminator (5).
1952#[cfg(has_io_uring)]
1953const TRAILER_LEN: usize = 7;
1954
1955#[cfg(has_io_uring)]
1956impl<'a> StreamValue<'a> {
1957    fn new(client: &'a mut Client, flags: u32, len: usize, leftover: Bytes) -> Self {
1958        Self {
1959            client,
1960            flags,
1961            len,
1962            value_left: len,
1963            trail_left: TRAILER_LEN,
1964            buf: leftover,
1965            finished: false,
1966        }
1967    }
1968
1969    /// The item flags from the `VALUE` header.
1970    pub fn flags(&self) -> u32 {
1971        self.flags
1972    }
1973
1974    /// The value length in bytes, from the `VALUE <key> <flags> <bytes>` header.
1975    pub fn len(&self) -> usize {
1976        self.len
1977    }
1978
1979    /// Whether the value is zero-length.
1980    pub fn is_empty(&self) -> bool {
1981        self.len == 0
1982    }
1983
1984    /// Pull the next non-empty received buffer into `self.buf`.
1985    ///
1986    /// A `None` from the runtime here means the peer closed (FIN) before the
1987    /// whole value + trailing delimiter arrived — a **short FIN**, surfaced as an
1988    /// error (never a truncated value), per the bounded-`len` contract.
1989    async fn refill(&mut self) -> Result<(), Error> {
1990        loop {
1991            match self.client.conn.recv_owned_segment().await {
1992                Ok(Some(b)) if !b.is_empty() => {
1993                    self.buf = b;
1994                    return Ok(());
1995                }
1996                Ok(Some(_)) => continue,
1997                Ok(None) => return Err(Error::ConnectionClosed),
1998                Err(e) => return Err(Error::Io(e)),
1999            }
2000        }
2001    }
2002
2003    /// The next chunk of the value body, or `Ok(None)` once the whole value has
2004    /// been delivered. Chunks are owned [`Bytes`] (Mode C copy-at-delivery); the
2005    /// caller may hold them freely.
2006    ///
2007    /// After the final chunk the trailing `\r\nEND\r\n` is consumed and the
2008    /// connection is restored for the next command, so a caller need not read
2009    /// past the last value byte — though calling again simply returns `Ok(None)`.
2010    pub async fn next_segment(&mut self) -> Result<Option<Bytes>, Error> {
2011        if self.value_left == 0 {
2012            self.finish().await?;
2013            return Ok(None);
2014        }
2015        if self.buf.is_empty() {
2016            self.refill().await?;
2017        }
2018        let take = self.value_left.min(self.buf.len());
2019        let chunk = self.buf.split_to(take);
2020        self.value_left -= take;
2021        if self.value_left == 0 {
2022            self.finish().await?;
2023        }
2024        Ok(Some(chunk))
2025    }
2026
2027    /// Materialize the whole value into one contiguous [`Bytes`] (the copy).
2028    ///
2029    /// Single-buffer values return an O(1) `Bytes` slice with no extra gather;
2030    /// multi-buffer values are gathered into one allocation (still one logical
2031    /// copy of the value from the caller's perspective).
2032    pub async fn collect(mut self) -> Result<Bytes, Error> {
2033        // Fast path: the whole value is already contiguous in `buf`.
2034        if self.buf.len() >= self.value_left {
2035            let out = self.buf.split_to(self.value_left);
2036            self.value_left = 0;
2037            self.finish().await?;
2038            return Ok(out);
2039        }
2040        let mut out = bytes::BytesMut::with_capacity(self.len);
2041        while self.value_left > 0 {
2042            if self.buf.is_empty() {
2043                self.refill().await?;
2044            }
2045            let take = self.value_left.min(self.buf.len());
2046            out.extend_from_slice(&self.buf.split_to(take));
2047            self.value_left -= take;
2048        }
2049        self.finish().await?;
2050        Ok(out.freeze())
2051    }
2052
2053    /// Consume and release the whole value without materializing it, leaving the
2054    /// connection usable for the next command. No gather copy.
2055    pub async fn discard(mut self) -> Result<(), Error> {
2056        while self.value_left > 0 {
2057            if self.buf.is_empty() {
2058                self.refill().await?;
2059            }
2060            let take = self.value_left.min(self.buf.len());
2061            let _ = self.buf.split_to(take);
2062            self.value_left -= take;
2063        }
2064        self.finish().await
2065    }
2066
2067    /// Consume the trailing `\r\nEND\r\n`, restore the default
2068    /// `with_data`/`with_bytes` read path, and mark the stream finished so `Drop`
2069    /// does not poison the connection.
2070    async fn finish(&mut self) -> Result<(), Error> {
2071        if self.finished {
2072            return Ok(());
2073        }
2074        while self.trail_left > 0 {
2075            if self.buf.is_empty() {
2076                self.refill().await?;
2077            }
2078            let take = self.trail_left.min(self.buf.len());
2079            let _ = self.buf.split_to(take);
2080            self.trail_left -= take;
2081        }
2082        // Restore the default read path. Any bytes still in `self.buf` past the
2083        // trailing delimiter would belong to a *following* reply — none exist in
2084        // the documented sequential use; they are dropped rather than reinjected.
2085        self.client.conn.end_segments()?;
2086        self.finished = true;
2087        Ok(())
2088    }
2089}
2090
2091#[cfg(has_io_uring)]
2092impl Drop for StreamValue<'_> {
2093    fn drop(&mut self) {
2094        if !self.finished {
2095            // Abandoned mid-value (or a short FIN left the stream broken):
2096            // undrained bytes remain inbound and the wire cannot be drained
2097            // synchronously here, so poison the connection. `close()` bumps the
2098            // slot generation, making the client's stored handle stale so the
2099            // next operation fails.
2100            self.client.conn.close();
2101        }
2102    }
2103}
2104
2105// ── Streaming get-with-CAS (segmented recv, single-connection Client only) ─
2106//
2107// `get_cas` is the CAS-carrying mirror of `get_stream`: it issues `gets <key>`
2108// instead of `get <key>`, so the hit reply is
2109// `VALUE <key> <flags> <bytes> <cas>\r\n<data>\r\nEND\r\n` (an extra `<cas>`
2110// token). The value body then streams exactly like `get_stream` — `get_cas`
2111// reuses `StreamValue` (bounded to `<bytes>`, `\r\nEND\r\n` trailer, short-FIN
2112// error, undrained-drop poison) and layers the parsed `cas` token on top.
2113
2114#[cfg(has_io_uring)]
2115impl Client {
2116    /// Streaming get-with-CAS (single-connection [`Client`] only).
2117    ///
2118    /// Like [`get_stream`](Self::get_stream), but issues `gets <key>` so the
2119    /// reply header carries a CAS token (`VALUE <key> <flags> <bytes> <cas>`),
2120    /// exposed via [`CasStreamValue::cas`]. The value body is delivered as a
2121    /// [`CasStreamValue`] of segments; a consumer that only discards or checksums
2122    /// pays no gather copy, and [`CasStreamValue::collect`] is the one
2123    /// materialization.
2124    ///
2125    /// Returns `Ok(None)` for a missing key (bare `END\r\n` reply).
2126    ///
2127    /// The same sequential-use, short-FIN-error, and undrained-drop-poison
2128    /// contracts as [`get_stream`](Self::get_stream) apply.
2129    pub async fn get_cas(
2130        &mut self,
2131        key: impl AsRef<[u8]>,
2132    ) -> Result<Option<CasStreamValue<'_>>, Error> {
2133        let key = key.as_ref();
2134        // Fire `gets <key>\r\n`, then opt into segmented delivery (synchronous,
2135        // before the await) so the reply arrives as held segments.
2136        self.encode_buf.clear();
2137        encode_request_into(&McRequest::gets(&[key]), &mut self.encode_buf)?;
2138        self.conn.send_nowait(&self.encode_buf)?;
2139
2140        // Read the `VALUE <key> <flags> <bytes> <cas>\r\n` header (or `END\r\n`).
2141        // Demarcation is exact — see `get_stream`.
2142        let mut acc: Option<Vec<u8>> = None;
2143        loop {
2144            let seg = match self.conn.recv_owned_segment().await {
2145                Ok(Some(b)) => b,
2146                Ok(None) => {
2147                    let _ = self.conn.end_segments();
2148                    return Err(Error::ConnectionClosed);
2149                }
2150                Err(e) => {
2151                    // Recv I/O error mid-header: the read is broken and the
2152                    // connection is still in the segmented domain. Poison it
2153                    // (close) so it is not reused stuck in that domain / desynced.
2154                    self.conn.close();
2155                    return Err(e.into());
2156                }
2157            };
2158            let combined: Bytes = match acc.take() {
2159                None => seg,
2160                Some(mut a) => {
2161                    a.extend_from_slice(&seg);
2162                    Bytes::from(a)
2163                }
2164            };
2165            match parse_gets_header(&combined) {
2166                Ok(Some(CasHeader::Miss)) => {
2167                    self.conn.end_segments()?;
2168                    return Ok(None);
2169                }
2170                Ok(Some(CasHeader::Value {
2171                    flags,
2172                    len,
2173                    cas,
2174                    header_len,
2175                })) => {
2176                    let leftover = combined.slice(header_len..);
2177                    let inner = StreamValue::new(self, flags, len, leftover);
2178                    return Ok(Some(CasStreamValue { inner, cas }));
2179                }
2180                Ok(None) => {
2181                    acc = Some(combined.to_vec());
2182                }
2183                Err(e) => {
2184                    let _ = self.conn.end_segments();
2185                    return Err(e);
2186                }
2187            }
2188        }
2189    }
2190}
2191
2192/// Parsed shape of a memcache `gets` reply header line (like [`GetHeader`] but
2193/// carrying the extra CAS token).
2194#[cfg(has_io_uring)]
2195enum CasHeader {
2196    /// Bare `END\r\n` — the key does not exist.
2197    Miss,
2198    /// `VALUE <key> <flags> <bytes> <cas>\r\n`.
2199    Value {
2200        flags: u32,
2201        len: usize,
2202        cas: u64,
2203        header_len: usize,
2204    },
2205}
2206
2207/// Parse a memcache `gets` reply header line from the **front** of `buf`,
2208/// measuring only the header line (5-token
2209/// `VALUE <key> <flags> <bytes> <cas>\r\n` / `END\r\n`). The value body and
2210/// trailing `\r\nEND\r\n` are left for the caller to stream.
2211///
2212/// Same return contract as [`parse_get_header`], with the additional `<cas>`
2213/// token extracted.
2214#[cfg(has_io_uring)]
2215fn parse_gets_header(buf: &[u8]) -> Result<Option<CasHeader>, Error> {
2216    let Some(cr) = find_crlf(buf) else {
2217        return Ok(None);
2218    };
2219    let line = &buf[..cr];
2220    let header_len = cr + 2; // line bytes + "\r\n"
2221
2222    if line == b"END" {
2223        return Ok(Some(CasHeader::Miss));
2224    }
2225    if line.starts_with(b"VALUE ") {
2226        // VALUE <key> <flags> <bytes> <cas>
2227        let mut fields = line.split(|&b| b == b' ').filter(|f| !f.is_empty());
2228        let _value_kw = fields.next(); // "VALUE"
2229        let _key = fields.next().ok_or(Error::UnexpectedResponse)?;
2230        let flags_tok = fields.next().ok_or(Error::UnexpectedResponse)?;
2231        let bytes_tok = fields.next().ok_or(Error::UnexpectedResponse)?;
2232        let cas_tok = fields.next().ok_or(Error::UnexpectedResponse)?;
2233        let flags = parse_uint::<u32>(flags_tok)?;
2234        let len = parse_uint::<usize>(bytes_tok)?;
2235        let cas = parse_uint::<u64>(cas_tok)?;
2236        return Ok(Some(CasHeader::Value {
2237            flags,
2238            len,
2239            cas,
2240            header_len,
2241        }));
2242    }
2243    if line == b"ERROR" || line.starts_with(b"CLIENT_ERROR") || line.starts_with(b"SERVER_ERROR") {
2244        return Err(Error::Memcache(String::from_utf8_lossy(line).into_owned()));
2245    }
2246    Err(Error::UnexpectedResponse)
2247}
2248
2249/// A streaming memcache get-with-CAS value (see [`Client::get_cas`]).
2250///
2251/// Wraps a [`StreamValue`] (the bounded value-body stream) with the CAS token
2252/// parsed from the `VALUE <key> <flags> <bytes> <cas>\r\n` header. All the
2253/// streaming semantics — bounded to the value length, short-FIN error,
2254/// undrained-drop poison — come from the inner [`StreamValue`].
2255#[cfg(has_io_uring)]
2256pub struct CasStreamValue<'a> {
2257    inner: StreamValue<'a>,
2258    cas: u64,
2259}
2260
2261#[cfg(has_io_uring)]
2262impl CasStreamValue<'_> {
2263    /// The item flags from the `VALUE` header.
2264    pub fn flags(&self) -> u32 {
2265        self.inner.flags()
2266    }
2267
2268    /// The CAS (compare-and-swap) unique token from the `VALUE` header.
2269    pub fn cas(&self) -> u64 {
2270        self.cas
2271    }
2272
2273    /// The value length in bytes, from the `VALUE <key> <flags> <bytes> <cas>`
2274    /// header.
2275    pub fn len(&self) -> usize {
2276        self.inner.len()
2277    }
2278
2279    /// Whether the value is zero-length.
2280    pub fn is_empty(&self) -> bool {
2281        self.inner.is_empty()
2282    }
2283
2284    /// The next chunk of the value body, or `Ok(None)` once the whole value has
2285    /// been delivered. See [`StreamValue::next_segment`].
2286    pub async fn next_segment(&mut self) -> Result<Option<Bytes>, Error> {
2287        self.inner.next_segment().await
2288    }
2289
2290    /// Materialize the whole value into one contiguous [`Bytes`] (the copy).
2291    /// See [`StreamValue::collect`].
2292    pub async fn collect(self) -> Result<Bytes, Error> {
2293        self.inner.collect().await
2294    }
2295
2296    /// Consume and release the whole value without materializing it, leaving the
2297    /// connection usable for the next command. See [`StreamValue::discard`].
2298    pub async fn discard(self) -> Result<(), Error> {
2299        self.inner.discard().await
2300    }
2301}
2302
2303// -- Zero-copy SET encoding helpers ------------------------------------------
2304
2305/// Append the memcache text SET prefix for guard-based sends to `buf`:
2306/// `set {key} {flags} {exptime} {valuelen}\r\n`.
2307/// The caller must append value bytes (via guard) + `\r\n` suffix.
2308///
2309/// Validates `key` (≤ [`MAX_KEY_LEN`]); returns [`Error::KeyTooLong`] if the
2310/// bound is exceeded so that no bytes hit the wire for a request the server
2311/// would misparse. Value length is not checked — see [`validate_request`].
2312/// On error nothing is appended to `buf`.
2313fn append_set_guard_prefix(
2314    buf: &mut Vec<u8>,
2315    key: &[u8],
2316    value_len: usize,
2317    flags: u32,
2318    exptime: u32,
2319) -> Result<(), Error> {
2320    validate_key(key)?;
2321    let mut itoa_buf = itoa::Buffer::new();
2322    buf.extend_from_slice(b"set ");
2323    buf.extend_from_slice(key);
2324    buf.push(b' ');
2325    buf.extend_from_slice(itoa_buf.format(flags).as_bytes());
2326    buf.push(b' ');
2327    buf.extend_from_slice(itoa_buf.format(exptime).as_bytes());
2328    buf.push(b' ');
2329    buf.extend_from_slice(itoa_buf.format(value_len).as_bytes());
2330    buf.extend_from_slice(b"\r\n");
2331    Ok(())
2332}
2333
2334// -- Encoding helpers --------------------------------------------------------
2335
2336/// Reject requests whose key exceeds the 250-byte protocol cap. Value
2337/// length is not checked here — memcached's `-I` item-size limit is a
2338/// server knob, so the client lets the server reject an oversized value
2339/// with a normal `SERVER_ERROR` reply rather than second-guessing it.
2340fn validate_request(req: &McRequest<'_>) -> Result<(), Error> {
2341    match req {
2342        McRequest::Get { key }
2343        | McRequest::Incr { key, .. }
2344        | McRequest::Decr { key, .. }
2345        | McRequest::Delete { key } => validate_key(key),
2346        McRequest::Gets { keys } => {
2347            for k in keys.iter() {
2348                validate_key(k)?;
2349            }
2350            Ok(())
2351        }
2352        McRequest::Set { key, .. }
2353        | McRequest::Add { key, .. }
2354        | McRequest::Replace { key, .. }
2355        | McRequest::Append { key, .. }
2356        | McRequest::Prepend { key, .. }
2357        | McRequest::Cas { key, .. } => validate_key(key),
2358        McRequest::FlushAll | McRequest::Version | McRequest::Quit => Ok(()),
2359    }
2360}
2361
2362/// Append the encoding of a `McRequest` to `buf`, rejecting oversized keys
2363/// up-front (see [`validate_request`]). No allocation once `buf` has
2364/// sufficient capacity. On error nothing is appended to `buf`.
2365pub(crate) fn encode_request_into(req: &McRequest<'_>, buf: &mut Vec<u8>) -> Result<(), Error> {
2366    validate_request(req)?;
2367    let size = match req {
2368        McRequest::Get { key } => 6 + key.len(),
2369        McRequest::Gets { keys } => 6 + keys.iter().map(|k| 1 + k.len()).sum::<usize>(),
2370        McRequest::Set { key, value, .. } | McRequest::Add { key, value, .. } => {
2371            41 + key.len() + value.len()
2372        }
2373        McRequest::Replace { key, value, .. } => 45 + key.len() + value.len(),
2374        McRequest::Incr { key, .. } | McRequest::Decr { key, .. } => 27 + key.len(),
2375        McRequest::Append { key, value } => 44 + key.len() + value.len(),
2376        McRequest::Prepend { key, value } => 45 + key.len() + value.len(),
2377        McRequest::Cas { key, value, .. } => 61 + key.len() + value.len(),
2378        McRequest::Delete { key } => 9 + key.len(),
2379        McRequest::FlushAll => 11,
2380        McRequest::Version => 9,
2381        McRequest::Quit => 6,
2382    };
2383    let start = buf.len();
2384    buf.resize(start + size, 0);
2385    let len = req.encode(&mut buf[start..]);
2386    buf.truncate(start + len);
2387    Ok(())
2388}
2389
2390/// Encode a `McRequest` into a freshly allocated `Vec<u8>`, rejecting
2391/// oversized keys up-front (see [`validate_request`]). Hot paths in
2392/// [`Client`] use [`encode_request_into`] with a reusable buffer instead.
2393pub(crate) fn encode_request(req: &McRequest<'_>) -> Result<Vec<u8>, Error> {
2394    let mut buf = Vec::new();
2395    encode_request_into(req, &mut buf)?;
2396    Ok(buf)
2397}
2398
2399/// Encode a SET command into a `Vec<u8>`. Test-only convenience; hot paths
2400/// use [`encode_request_into`] with a reused buffer.
2401#[cfg(test)]
2402pub(crate) fn encode_set(
2403    key: &[u8],
2404    value: &[u8],
2405    flags: u32,
2406    exptime: u32,
2407) -> Result<Vec<u8>, Error> {
2408    encode_request(&McRequest::Set {
2409        key,
2410        value,
2411        flags,
2412        exptime,
2413    })
2414}
2415
2416/// Encode an ADD command into a `Vec<u8>`.
2417pub(crate) fn encode_add(key: &[u8], value: &[u8]) -> Result<Vec<u8>, Error> {
2418    encode_request(&McRequest::Add {
2419        key,
2420        value,
2421        flags: 0,
2422        exptime: 0,
2423    })
2424}
2425
2426/// Check a `ResponseBytes` for error variants and return an appropriate `Error`.
2427pub(crate) fn check_error_bytes(response: &McResponseBytes) -> Result<(), Error> {
2428    match response {
2429        McResponseBytes::Error => Err(Error::Memcache("ERROR".into())),
2430        McResponseBytes::ClientError(msg) => Err(Error::Memcache(format!(
2431            "CLIENT_ERROR {}",
2432            String::from_utf8_lossy(msg)
2433        ))),
2434        McResponseBytes::ServerError(msg) => Err(Error::Memcache(format!(
2435            "SERVER_ERROR {}",
2436            String::from_utf8_lossy(msg)
2437        ))),
2438        _ => Ok(()),
2439    }
2440}
2441
2442// ── Kernel timestamp helper ─────────────────────────────────────────────
2443
2444#[cfg(feature = "timestamps")]
2445fn now_realtime_ns() -> u64 {
2446    let mut ts = libc::timespec {
2447        tv_sec: 0,
2448        tv_nsec: 0,
2449    };
2450    unsafe {
2451        libc::clock_gettime(libc::CLOCK_REALTIME, &mut ts);
2452    }
2453    ts.tv_sec as u64 * 1_000_000_000 + ts.tv_nsec as u64
2454}
2455
2456#[cfg(test)]
2457mod encode_tests {
2458    use super::*;
2459
2460    // 14-byte key with digits — exercises digit/key-byte adjacency and
2461    // multi-digit integer fields (itoa boundary cases).
2462    const KEY: &[u8] = b"user:123456789";
2463
2464    #[test]
2465    fn golden_encode_request_get() {
2466        let encoded = encode_request(&McRequest::get(KEY)).unwrap();
2467        assert_eq!(encoded, b"get user:123456789\r\n");
2468    }
2469
2470    #[test]
2471    fn golden_encode_request_delete() {
2472        let encoded = encode_request(&McRequest::delete(KEY)).unwrap();
2473        assert_eq!(encoded, b"delete user:123456789\r\n");
2474    }
2475
2476    #[test]
2477    fn golden_encode_request_set() {
2478        let encoded = encode_set(KEY, b"hello world value", 42, 7200).unwrap();
2479        assert_eq!(
2480            encoded,
2481            &b"set user:123456789 42 7200 17\r\nhello world value\r\n"[..]
2482        );
2483    }
2484
2485    #[test]
2486    fn golden_encode_request_set_zero_fields() {
2487        let encoded = encode_set(KEY, b"v", 0, 0).unwrap();
2488        assert_eq!(encoded, &b"set user:123456789 0 0 1\r\nv\r\n"[..]);
2489    }
2490
2491    #[test]
2492    fn golden_set_guard_prefix() {
2493        // Non-trivial flags / exptime / value_len so every integer field
2494        // is pinned byte-exactly.
2495        let mut prefix = Vec::new();
2496        append_set_guard_prefix(&mut prefix, KEY, 1024, 42, 7200).unwrap();
2497        assert_eq!(prefix, &b"set user:123456789 42 7200 1024\r\n"[..]);
2498    }
2499
2500    #[test]
2501    fn encode_into_appends_without_clobbering() {
2502        // The `_into` helper must append, never clear — clearing is the
2503        // caller's responsibility.
2504        let mut buf = b"EXISTING".to_vec();
2505        encode_request_into(&McRequest::get(KEY), &mut buf).unwrap();
2506        assert_eq!(buf, &b"EXISTINGget user:123456789\r\n"[..]);
2507    }
2508
2509    #[test]
2510    fn encode_into_error_leaves_buf_untouched() {
2511        let mut buf = b"EXISTING".to_vec();
2512        let long_key = vec![b'k'; MAX_KEY_LEN + 1];
2513        assert!(encode_request_into(&McRequest::get(&long_key), &mut buf).is_err());
2514        assert_eq!(buf, b"EXISTING");
2515        assert!(append_set_guard_prefix(&mut buf, &long_key, 16, 0, 0).is_err());
2516        assert_eq!(buf, b"EXISTING");
2517    }
2518
2519    // ── Coalescing byte-exactness ───────────────────────────────────────
2520    //
2521    // The write-coalescing layer (`max_batch_size > 1`) builds a single
2522    // `write_buf` by appending each `fire_*` command's encoding back-to-back,
2523    // then issues one send. The on-wire bytes for a coalesced batch must
2524    // therefore equal the exact concatenation of the N individual command
2525    // encodings. These tests pin that invariant without a live connection by
2526    // exercising the same `encode_request_into` append path `fire_*` uses.
2527
2528    #[test]
2529    fn coalesced_buf_equals_concatenation_of_individual_encodings() {
2530        // Build the batch the way the coalescing path does: append each
2531        // command into one shared buffer.
2532        let mut coalesced = Vec::new();
2533        encode_request_into(&McRequest::get(b"key1"), &mut coalesced).unwrap();
2534        encode_request_into(
2535            &McRequest::Set {
2536                key: b"key2",
2537                value: b"val",
2538                flags: 0,
2539                exptime: 0,
2540            },
2541            &mut coalesced,
2542        )
2543        .unwrap();
2544        encode_request_into(&McRequest::delete(b"key3"), &mut coalesced).unwrap();
2545
2546        // Build the reference: concatenate three independently-encoded
2547        // commands.
2548        let mut expected = Vec::new();
2549        expected.extend_from_slice(&encode_request(&McRequest::get(b"key1")).unwrap());
2550        expected.extend_from_slice(
2551            &encode_request(&McRequest::Set {
2552                key: b"key2",
2553                value: b"val",
2554                flags: 0,
2555                exptime: 0,
2556            })
2557            .unwrap(),
2558        );
2559        expected.extend_from_slice(&encode_request(&McRequest::delete(b"key3")).unwrap());
2560
2561        assert_eq!(coalesced, expected);
2562        // And the literal on-wire bytes, to catch framing regressions.
2563        assert_eq!(
2564            coalesced,
2565            &b"get key1\r\nset key2 0 0 3\r\nval\r\ndelete key3\r\n"[..]
2566        );
2567    }
2568
2569    #[test]
2570    fn coalesced_guard_set_byte_layout() {
2571        // Mirror the `fire_set_with_guard` buffer assembly: prefix into
2572        // write_buf, guard insertion point recorded, then "\r\n" suffix.
2573        // The flush scatter-gather then emits: prefix || value || "\r\n".
2574        let mut write_buf = Vec::new();
2575        let value: &[u8] = b"hello";
2576        append_set_guard_prefix(&mut write_buf, KEY, value.len(), 42, 7200).unwrap();
2577        let guard_offset = write_buf.len();
2578        write_buf.extend_from_slice(b"\r\n");
2579
2580        // Reconstruct the on-wire stream the flush path produces:
2581        // Copy(write_buf[..guard_offset]) Guard(value) Copy(write_buf[guard_offset..]).
2582        let mut wire = Vec::new();
2583        wire.extend_from_slice(&write_buf[..guard_offset]);
2584        wire.extend_from_slice(value);
2585        wire.extend_from_slice(&write_buf[guard_offset..]);
2586
2587        assert_eq!(
2588            wire,
2589            &b"set user:123456789 42 7200 5\r\nhello\r\n"[..],
2590            "coalesced guard SET must match a standard SET on the wire"
2591        );
2592
2593        // tx_bytes accounting: write_buf bytes + value bytes (guard is not
2594        // in write_buf). `fire_set_with_guard` computes
2595        // (write_buf.len() - before + value_len).
2596        assert_eq!(write_buf.len() + value.len(), wire.len());
2597    }
2598
2599    #[test]
2600    fn two_coalesced_guard_sets_interleave_correctly() {
2601        // Two guard SETs in one batch must produce
2602        // prefix1 value1 "\r\n" prefix2 value2 "\r\n" on the wire — this
2603        // pins the drain/interleave loop in `flush()`.
2604        let v1: &[u8] = b"aa";
2605        let v2: &[u8] = b"bbbb";
2606
2607        let mut write_buf = Vec::new();
2608        let mut guards: Vec<usize> = Vec::new();
2609
2610        append_set_guard_prefix(&mut write_buf, b"k1", v1.len(), 0, 0).unwrap();
2611        guards.push(write_buf.len());
2612        write_buf.extend_from_slice(b"\r\n");
2613
2614        append_set_guard_prefix(&mut write_buf, b"k2", v2.len(), 0, 0).unwrap();
2615        guards.push(write_buf.len());
2616        write_buf.extend_from_slice(b"\r\n");
2617
2618        // Replay the flush interleave loop.
2619        let values = [v1, v2];
2620        let mut wire = Vec::new();
2621        let mut pos = 0;
2622        for (i, &offset) in guards.iter().enumerate() {
2623            if offset > pos {
2624                wire.extend_from_slice(&write_buf[pos..offset]);
2625            }
2626            wire.extend_from_slice(values[i]);
2627            pos = offset;
2628        }
2629        if pos < write_buf.len() {
2630            wire.extend_from_slice(&write_buf[pos..]);
2631        }
2632
2633        assert_eq!(wire, &b"set k1 0 0 2\r\naa\r\nset k2 0 0 4\r\nbbbb\r\n"[..]);
2634    }
2635}
2636
2637#[cfg(test)]
2638mod tests {
2639    use super::*;
2640
2641    #[test]
2642    fn validate_key_accepts_max_len() {
2643        let key = vec![b'k'; MAX_KEY_LEN];
2644        assert!(validate_key(&key).is_ok());
2645    }
2646
2647    #[test]
2648    fn validate_key_rejects_oversized() {
2649        let key = vec![b'k'; MAX_KEY_LEN + 1];
2650        assert!(matches!(validate_key(&key), Err(Error::KeyTooLong)));
2651    }
2652
2653    #[test]
2654    fn encode_request_get_rejects_long_key() {
2655        let key = vec![b'k'; MAX_KEY_LEN + 1];
2656        let r = encode_request(&McRequest::get(&key));
2657        assert!(matches!(r, Err(Error::KeyTooLong)));
2658    }
2659
2660    #[test]
2661    fn encode_request_set_passes_large_value() {
2662        // Value length is not checked client-side — memcached's `-I` item-size
2663        // limit is a server knob, so an oversized value is allowed onto the
2664        // wire and the server rejects it.
2665        let key = b"k";
2666        let value = vec![0u8; 2 * 1024 * 1024];
2667        let r = encode_request(&McRequest::Set {
2668            key,
2669            value: &value,
2670            flags: 0,
2671            exptime: 0,
2672        });
2673        assert!(r.is_ok());
2674        assert!(r.unwrap().starts_with(b"set "));
2675    }
2676
2677    #[test]
2678    fn encode_request_gets_rejects_any_long_key() {
2679        let ok = vec![b'k'; MAX_KEY_LEN];
2680        let bad = vec![b'k'; MAX_KEY_LEN + 1];
2681        let keys: &[&[u8]] = &[&ok, &bad];
2682        let r = encode_request(&McRequest::gets(keys));
2683        assert!(matches!(r, Err(Error::KeyTooLong)));
2684    }
2685
2686    #[test]
2687    fn set_guard_prefix_rejects_long_key() {
2688        let key = vec![b'k'; MAX_KEY_LEN + 1];
2689        let r = append_set_guard_prefix(&mut Vec::new(), &key, 16, 0, 0);
2690        assert!(matches!(r, Err(Error::KeyTooLong)));
2691    }
2692
2693    #[test]
2694    fn set_guard_prefix_passes_large_value_len() {
2695        // Value length is not checked client-side (see `validate_request`).
2696        let r = append_set_guard_prefix(&mut Vec::new(), b"k", 2 * 1024 * 1024, 0, 0);
2697        assert!(r.is_ok());
2698    }
2699
2700    #[test]
2701    fn encode_request_passes_through_at_key_cap() {
2702        let key = vec![b'k'; MAX_KEY_LEN];
2703        let value = vec![0u8; 1024];
2704        let r = encode_request(&McRequest::Set {
2705            key: &key,
2706            value: &value,
2707            flags: 0,
2708            exptime: 0,
2709        });
2710        assert!(r.is_ok());
2711        let buf = r.unwrap();
2712        // Sanity: encoded buffer starts with "set ".
2713        assert!(buf.starts_with(b"set "));
2714    }
2715}
2716
2717#[cfg(test)]
2718mod zc_threshold_tests {
2719    use super::*;
2720    use ringline::{ConnCtx, RegionId, SendGuard};
2721
2722    const KEY: &[u8] = b"user:123456789";
2723
2724    /// Heap-backed guard for tests. The `Vec` keeps bytes alive for the
2725    /// guard's lifetime.
2726    struct VecGuard(Vec<u8>);
2727
2728    impl SendGuard for VecGuard {
2729        fn as_ptr_len(&self) -> (*const u8, u32) {
2730            (self.0.as_ptr(), self.0.len() as u32)
2731        }
2732        fn region(&self) -> RegionId {
2733            RegionId::UNREGISTERED
2734        }
2735    }
2736
2737    /// A `Client` backed by a dangling `ConnCtx`. Only safe for tests that
2738    /// stay in the buffered path (no flush, no direct send, no recv).
2739    fn test_client(max_batch_size: usize, zc_threshold: u32) -> Client {
2740        let conn = ConnCtx::for_test(0, 0);
2741        Client::builder(conn)
2742            .max_batch_size(max_batch_size)
2743            .zc_threshold(zc_threshold)
2744            .build()
2745    }
2746
2747    #[test]
2748    fn small_guard_set_is_byte_identical_to_plain_set() {
2749        // Small guarded SET must produce byte-identical write_buf to a plain
2750        // copy SET of the same key/value/flags/exptime via fire_set.
2751        let value = vec![0xABu8; 64];
2752
2753        let mut guarded = test_client(4, 4096);
2754        guarded
2755            .fire_set_with_guard(KEY, VecGuard(value.clone()), 7, 42, 1)
2756            .unwrap();
2757
2758        let mut plain = test_client(4, 4096);
2759        plain.fire_set(KEY, &value, 7, 42, 1).unwrap();
2760
2761        assert_eq!(guarded.write_buf, plain.write_buf);
2762        assert!(guarded.write_guards.is_empty());
2763    }
2764
2765    #[test]
2766    fn small_value_takes_copy_path() {
2767        // 64B < 4096 → folds into copy path: write_guards empty, one buffered op.
2768        let mut client = test_client(4, 4096);
2769        client
2770            .fire_set_with_guard(KEY, VecGuard(vec![0u8; 64]), 0, 0, 1)
2771            .unwrap();
2772        assert!(client.write_guards.is_empty());
2773        assert_eq!(client.buffered_ops, 1);
2774        assert!(!client.write_buf.is_empty());
2775    }
2776
2777    #[test]
2778    fn large_value_takes_guard_path() {
2779        // 8KB >= 4096 → guard path: write_guards non-empty.
2780        let mut client = test_client(4, 4096);
2781        client
2782            .fire_set_with_guard(KEY, VecGuard(vec![0u8; 8192]), 0, 0, 1)
2783            .unwrap();
2784        assert_eq!(client.write_guards.len(), 1);
2785    }
2786
2787    #[test]
2788    fn threshold_zero_disables_fold() {
2789        // zc_threshold = 0 → always guard path, even for tiny values.
2790        let mut client = test_client(4, 0);
2791        client
2792            .fire_set_with_guard(KEY, VecGuard(vec![0u8; 64]), 0, 0, 1)
2793            .unwrap();
2794        assert_eq!(client.write_guards.len(), 1);
2795    }
2796
2797    #[test]
2798    fn guard_batch_accumulates_max_flush_guards_before_flush() {
2799        // zc_threshold = 0 forces the guard path. MAX_FLUSH_GUARDS guards
2800        // must accumulate without triggering a pre-flush: g guards need
2801        // 2g+1 iovecs, within MAX_FLUSH_IOVECS. A stale iovec cap of 8
2802        // used to force a flush at the 4th guard (batching only 3).
2803        let mut client = test_client(16, 0);
2804        for i in 0..MAX_FLUSH_GUARDS {
2805            client
2806                .fire_set_with_guard(KEY, VecGuard(vec![0u8; 64]), 0, 0, i as u64)
2807                .unwrap();
2808        }
2809        assert_eq!(
2810            client.write_guards.len(),
2811            MAX_FLUSH_GUARDS,
2812            "a full guard batch must accumulate without a premature flush"
2813        );
2814    }
2815
2816    #[test]
2817    fn batch_of_small_guard_sets_coalesces() {
2818        // N small guarded SETs at max_batch_size = N: all fold into the copy
2819        // path (write_guards stays empty), so they batch into a single buffer.
2820        // With a guard path they'd flush every MAX_FLUSH_GUARDS ops.
2821        const N: usize = 8;
2822        let mut client = test_client(N, 4096);
2823        for i in 0..N - 1 {
2824            client
2825                .fire_set_with_guard(KEY, VecGuard(vec![0u8; 64]), 0, 0, i as u64)
2826                .unwrap();
2827            assert!(
2828                client.write_guards.is_empty(),
2829                "op {i} must use the copy path, not the guard path"
2830            );
2831            assert_eq!(client.buffered_ops, i + 1);
2832        }
2833        assert_eq!(client.buffered_ops, N - 1);
2834        assert!(client.write_guards.is_empty());
2835    }
2836}