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    /// Value exceeds memcache's default `-I` 1 MiB cap ([`MAX_VALUE_LEN`]).
205    /// Same rationale as [`Error::KeyTooLong`].
206    #[error("value too long (max 1048576 bytes)")]
207    ValueTooLong,
208}
209
210/// Maximum key length per memcache text-protocol spec.
211pub const MAX_KEY_LEN: usize = 250;
212
213/// Maximum value length matching the parser cap in `memcache-proto`
214/// (`MAX_VALUE_DATA_LEN`) and memcached's default `-I` 1 MiB item size.
215pub const MAX_VALUE_LEN: usize = 1024 * 1024;
216
217#[inline]
218fn validate_key(key: &[u8]) -> Result<(), Error> {
219    if key.len() > MAX_KEY_LEN {
220        Err(Error::KeyTooLong)
221    } else {
222        Ok(())
223    }
224}
225
226#[inline]
227fn validate_value(value: &[u8]) -> Result<(), Error> {
228    if value.len() > MAX_VALUE_LEN {
229        Err(Error::ValueTooLong)
230    } else {
231        Ok(())
232    }
233}
234
235#[inline]
236fn validate_value_len(value_len: usize) -> Result<(), Error> {
237    if value_len > MAX_VALUE_LEN {
238        Err(Error::ValueTooLong)
239    } else {
240        Ok(())
241    }
242}
243
244// -- Value types -------------------------------------------------------------
245
246/// A value returned from a single-key GET command.
247#[derive(Debug, Clone)]
248pub struct Value {
249    /// The cached data.
250    pub data: Bytes,
251    /// Flags stored with the item.
252    pub flags: u32,
253}
254
255/// A value returned from a multi-key GET command, including the key.
256#[derive(Debug, Clone)]
257pub struct GetValue {
258    /// The key for this value.
259    pub key: Bytes,
260    /// The cached data.
261    pub data: Bytes,
262    /// Flags stored with the item.
263    pub flags: u32,
264    /// CAS unique token (present when the server returns it via `gets`).
265    pub cas: Option<u64>,
266}
267
268// ── Command types ───────────────────────────────────────────────────────
269
270/// The type of Memcache command that completed.
271#[derive(Debug, Clone, Copy, PartialEq, Eq)]
272#[non_exhaustive]
273pub enum CommandType {
274    Get,
275    Set,
276    Delete,
277    Other,
278}
279
280/// Result metadata for a completed command, passed to the `on_result` callback.
281#[derive(Debug, Clone)]
282pub struct CommandResult {
283    /// The command type.
284    pub command: CommandType,
285    /// Latency in nanoseconds (send → response parsed).
286    pub latency_ns: u64,
287    /// For GET: `Some(true)` = hit, `Some(false)` = miss. `None` for others.
288    pub hit: Option<bool>,
289    /// Whether the command succeeded (no error response).
290    pub success: bool,
291    /// Time-to-first-byte in nanoseconds (not available in sequential mode).
292    pub ttfb_ns: Option<u64>,
293    /// Bytes transmitted for this command (protocol-encoded request size).
294    pub tx_bytes: u32,
295    /// Bytes received for this command (protocol-encoded response size).
296    pub rx_bytes: u32,
297}
298
299// ── ClientMetrics ───────────────────────────────────────────────────────
300
301/// Built-in histogram-based metrics, available when the `metrics` feature is
302/// enabled. Not registered globally — the caller decides how to expose them.
303#[cfg(feature = "metrics")]
304pub struct ClientMetrics {
305    /// Overall request latency histogram.
306    pub latency: histogram::Histogram,
307    /// GET latency histogram.
308    pub get_latency: histogram::Histogram,
309    /// SET latency histogram.
310    pub set_latency: histogram::Histogram,
311    /// DEL latency histogram.
312    pub del_latency: histogram::Histogram,
313    /// Total requests completed.
314    pub requests: u64,
315    /// Total errors.
316    pub errors: u64,
317    /// Total GET hits.
318    pub hits: u64,
319    /// Total GET misses.
320    pub misses: u64,
321}
322
323#[cfg(feature = "metrics")]
324impl ClientMetrics {
325    fn new() -> Self {
326        Self {
327            latency: histogram::Histogram::new(7, 64).unwrap(),
328            get_latency: histogram::Histogram::new(7, 64).unwrap(),
329            set_latency: histogram::Histogram::new(7, 64).unwrap(),
330            del_latency: histogram::Histogram::new(7, 64).unwrap(),
331            requests: 0,
332            errors: 0,
333            hits: 0,
334            misses: 0,
335        }
336    }
337
338    fn record(&mut self, result: &CommandResult) {
339        self.requests += 1;
340        let _ = self.latency.increment(result.latency_ns);
341
342        if !result.success {
343            self.errors += 1;
344        }
345
346        match result.command {
347            CommandType::Get => {
348                let _ = self.get_latency.increment(result.latency_ns);
349                match result.hit {
350                    Some(true) => self.hits += 1,
351                    Some(false) => self.misses += 1,
352                    None => {}
353                }
354            }
355            CommandType::Set => {
356                let _ = self.set_latency.increment(result.latency_ns);
357            }
358            CommandType::Delete => {
359                let _ = self.del_latency.increment(result.latency_ns);
360            }
361            _ => {}
362        }
363    }
364}
365
366// ── Pending operation state ─────────────────────────────────────────────
367
368enum PendingOpKind {
369    Get,
370    Set,
371    Delete,
372}
373
374struct PendingOp {
375    kind: PendingOpKind,
376    send_ts: u64,
377    start: Option<Instant>,
378    user_data: u64,
379    tx_bytes: u32,
380}
381
382/// A completed fire/recv operation with its result.
383#[non_exhaustive]
384pub enum CompletedOp {
385    /// GET completed.
386    Get {
387        result: Result<Option<Value>, Error>,
388        user_data: u64,
389        latency_ns: u64,
390    },
391    /// SET completed.
392    Set {
393        result: Result<(), Error>,
394        user_data: u64,
395        latency_ns: u64,
396    },
397    /// DELETE completed.
398    Delete {
399        result: Result<bool, Error>,
400        user_data: u64,
401        latency_ns: u64,
402    },
403}
404
405// ── ClientBuilder ───────────────────────────────────────────────────────
406
407/// Builder for creating a [`Client`] with per-request callbacks and metrics.
408pub struct ClientBuilder {
409    conn: ConnCtx,
410    on_result: Option<ResultCallback>,
411    max_batch_size: usize,
412    max_in_flight: usize,
413    zc_threshold: u32,
414    #[cfg(feature = "timestamps")]
415    use_kernel_ts: bool,
416    #[cfg(feature = "metrics")]
417    with_metrics: bool,
418}
419
420impl ClientBuilder {
421    pub(crate) fn new(conn: ConnCtx) -> Self {
422        Self {
423            conn,
424            on_result: None,
425            max_batch_size: 1,
426            max_in_flight: usize::MAX,
427            zc_threshold: DEFAULT_ZC_THRESHOLD,
428            #[cfg(feature = "timestamps")]
429            use_kernel_ts: false,
430            #[cfg(feature = "metrics")]
431            with_metrics: false,
432        }
433    }
434
435    /// Set the zero-copy guard threshold (in bytes). Guard values passed to
436    /// [`fire_set_with_guard`](Client::fire_set_with_guard) *smaller* than
437    /// this are copied into the coalescing send buffer so they batch like
438    /// plain SETs instead of taking the scatter-gather guard path (which
439    /// flushes every few ops). `0` = always use the guard path.
440    ///
441    /// Defaults to 4096. Should generally match the runtime
442    /// `Config::send_zc_threshold`.
443    pub fn zc_threshold(mut self, bytes: u32) -> Self {
444        self.zc_threshold = bytes;
445        self
446    }
447
448    /// Configure the maximum number of in-flight `fire_*` operations.
449    /// `fire_*` returns [`Error::TooManyInFlight`] past it. Defaults to
450    /// `usize::MAX` (unbounded). Set a bounded value on any server that
451    /// issues `fire_*` faster than `recv()` consumes.
452    pub fn max_in_flight(mut self, n: usize) -> Self {
453        self.max_in_flight = n;
454        self
455    }
456
457    /// Register a callback invoked after each command completes.
458    pub fn on_result<F: Fn(&CommandResult) + 'static>(mut self, f: F) -> Self {
459        self.on_result = Some(Box::new(f));
460        self
461    }
462
463    /// Enable kernel SO_TIMESTAMPING for latency measurement (requires `timestamps` feature).
464    #[cfg(feature = "timestamps")]
465    pub fn kernel_timestamps(mut self, enabled: bool) -> Self {
466        self.use_kernel_ts = enabled;
467        self
468    }
469
470    /// Set the maximum number of `fire_*` commands to coalesce into a single
471    /// send. Default is 1 (each `fire_*` sends immediately, matching
472    /// pre-coalescing behavior). Set higher for pipelined workloads to batch
473    /// multiple commands into fewer TCP segments.
474    ///
475    /// # Panics
476    ///
477    /// Panics if `n` is 0.
478    pub fn max_batch_size(mut self, n: usize) -> Self {
479        assert!(n > 0, "max_batch_size must be >= 1");
480        self.max_batch_size = n;
481        self
482    }
483
484    /// Enable built-in histogram tracking (requires `metrics` feature).
485    #[cfg(feature = "metrics")]
486    pub fn with_metrics(mut self) -> Self {
487        self.with_metrics = true;
488        self
489    }
490
491    /// Build the client.
492    pub fn build(self) -> Client {
493        Client {
494            conn: self.conn,
495            on_result: self.on_result,
496            pending: VecDeque::with_capacity(16),
497            last_rx_bytes: Cell::new(0),
498            write_buf: Vec::new(),
499            write_guards: Vec::new(),
500            flushed_count: 0,
501            max_batch_size: self.max_batch_size,
502            max_in_flight: self.max_in_flight,
503            zc_threshold: self.zc_threshold,
504            buffered_ops: 0,
505            encode_buf: Vec::new(),
506            #[cfg(feature = "timestamps")]
507            use_kernel_ts: self.use_kernel_ts,
508            #[cfg(feature = "metrics")]
509            metrics: if self.with_metrics {
510                Some(ClientMetrics::new())
511            } else {
512                None
513            },
514        }
515    }
516}
517
518// -- Client ------------------------------------------------------------------
519
520/// A ringline-native Memcache client wrapping a single connection.
521///
522/// `Client::new(conn)` creates a zero-overhead client with no callbacks or
523/// metrics. Use `Client::builder(conn)` to configure per-request callbacks,
524/// kernel timestamps, and built-in histogram tracking.
525pub struct Client {
526    conn: ConnCtx,
527    on_result: Option<ResultCallback>,
528    pending: VecDeque<PendingOp>,
529    last_rx_bytes: Cell<u32>,
530    /// Write buffer for coalescing `fire_*` commands. Contains all copy data
531    /// (command framing, prefixes, suffixes, non-guard values). Guard values
532    /// are stored separately in `write_guards` with byte offsets into this buffer.
533    write_buf: Vec<u8>,
534    /// Zero-copy guards pending flush. Each entry is `(offset, guard)` where
535    /// `offset` is the byte position in `write_buf` where the guard value
536    /// should be inserted in the byte stream.
537    write_guards: Vec<(usize, GuardBox)>,
538    /// Number of pending ops whose send_ts has been finalized (at flush time).
539    flushed_count: usize,
540    /// Maximum `fire_*` commands to coalesce before flushing. 1 = send each
541    /// command immediately (default, matching pre-coalescing behavior).
542    max_batch_size: usize,
543    /// Cap on `pending.len()`; `fire_*` returns `Error::TooManyInFlight`
544    /// past it. `usize::MAX` (default) disables.
545    max_in_flight: usize,
546    /// Guard values smaller than this (bytes) are folded into the coalescing
547    /// copy path by `fire_set_with_guard` instead of taking the
548    /// scatter-gather guard path. `0` disables the fold.
549    zc_threshold: u32,
550    /// Number of ops buffered in `write_buf` that have not yet been flushed.
551    buffered_ops: usize,
552    /// Reusable scratch buffer for encoding requests. Cleared before each
553    /// use; keeps its capacity across requests so steady-state encoding is
554    /// allocation-free.
555    encode_buf: Vec<u8>,
556    #[cfg(feature = "timestamps")]
557    use_kernel_ts: bool,
558    #[cfg(feature = "metrics")]
559    metrics: Option<ClientMetrics>,
560}
561
562impl Client {
563    /// Create a new client wrapping an established connection.
564    ///
565    /// No callbacks, no metrics, no kernel timestamps — zero overhead.
566    pub fn new(conn: ConnCtx) -> Self {
567        Self {
568            conn,
569            on_result: None,
570            pending: VecDeque::new(),
571            last_rx_bytes: Cell::new(0),
572            write_buf: Vec::new(),
573            write_guards: Vec::new(),
574            flushed_count: 0,
575            max_batch_size: 1,
576            max_in_flight: usize::MAX,
577            zc_threshold: DEFAULT_ZC_THRESHOLD,
578            buffered_ops: 0,
579            encode_buf: Vec::new(),
580            #[cfg(feature = "timestamps")]
581            use_kernel_ts: false,
582            #[cfg(feature = "metrics")]
583            metrics: None,
584        }
585    }
586
587    /// Create a builder for a client with per-request callbacks.
588    pub fn builder(conn: ConnCtx) -> ClientBuilder {
589        ClientBuilder::new(conn)
590    }
591
592    /// Returns the underlying connection context.
593    pub fn conn(&self) -> ConnCtx {
594        self.conn
595    }
596
597    /// Returns a reference to the built-in metrics, if enabled.
598    #[cfg(feature = "metrics")]
599    pub fn metrics(&self) -> Option<&ClientMetrics> {
600        self.metrics.as_ref()
601    }
602
603    /// Returns a mutable reference to the built-in metrics, if enabled.
604    #[cfg(feature = "metrics")]
605    pub fn metrics_mut(&mut self) -> Option<&mut ClientMetrics> {
606        self.metrics.as_mut()
607    }
608
609    // ── Timing helpers (private) ────────────────────────────────────────
610
611    #[inline]
612    fn is_instrumented(&self) -> bool {
613        if self.on_result.is_some() {
614            return true;
615        }
616        #[cfg(feature = "metrics")]
617        if self.metrics.is_some() {
618            return true;
619        }
620        false
621    }
622
623    #[cfg(feature = "timestamps")]
624    #[inline]
625    fn send_timestamp(&self) -> u64 {
626        if self.use_kernel_ts {
627            now_realtime_ns()
628        } else {
629            0
630        }
631    }
632
633    #[cfg(not(feature = "timestamps"))]
634    #[inline]
635    fn send_timestamp(&self) -> u64 {
636        0
637    }
638
639    #[cfg(feature = "timestamps")]
640    #[inline]
641    fn finish_timing(&self, send_ts: u64, start: Instant) -> u64 {
642        if self.use_kernel_ts {
643            let recv_ts = self.conn.recv_timestamp();
644            if recv_ts > 0 && recv_ts > send_ts {
645                return recv_ts - send_ts;
646            }
647        }
648        start.elapsed().as_nanos() as u64
649    }
650
651    #[cfg(not(feature = "timestamps"))]
652    #[inline]
653    fn finish_timing(&self, _send_ts: u64, start: Instant) -> u64 {
654        start.elapsed().as_nanos() as u64
655    }
656
657    fn record(&mut self, result: &CommandResult) {
658        if let Some(ref cb) = self.on_result {
659            cb(result);
660        }
661        #[cfg(feature = "metrics")]
662        if let Some(ref mut m) = self.metrics {
663            m.record(result);
664        }
665    }
666
667    // ── Fire/recv pipelining API ─────────────────────────────────────────
668
669    #[inline]
670    fn timing_start(&self) -> (u64, Option<Instant>) {
671        #[cfg(feature = "timestamps")]
672        {
673            if self.is_instrumented() {
674                (self.send_timestamp(), Some(Instant::now()))
675            } else {
676                (0, None)
677            }
678        }
679        #[cfg(not(feature = "timestamps"))]
680        {
681            // When timestamps feature is disabled, only use Instant::now() if callbacks are registered
682            if self.on_result.is_some() {
683                (0, Some(Instant::now()))
684            } else {
685                (0, None)
686            }
687        }
688    }
689
690    /// `timing_start()` for `fire_*` call sites, evaluated *after* the op
691    /// has been buffered/sent. When this op is buffered behind another op
692    /// (`buffered_ops > 1`), `flush()` will unconditionally re-stamp
693    /// `send_ts`/`start` for every op in the batch, so the fire-time clock
694    /// read would be discarded — skip it. `buffered_ops` is monotonic
695    /// between flushes, so if it is > 1 now, the rewriting branch in
696    /// `flush()` (`buffered_ops > 1`) is guaranteed to run for this op.
697    #[inline]
698    fn timing_start_buffered(&self) -> (u64, Option<Instant>) {
699        if self.buffered_ops >= 1 {
700            (0, None)
701        } else {
702            self.timing_start()
703        }
704    }
705
706    #[cfg(feature = "timestamps")]
707    #[inline]
708    fn compute_ttfb(&self, send_ts: u64) -> Option<u64> {
709        if self.use_kernel_ts {
710            let recv_ts = self.conn.recv_timestamp();
711            if recv_ts > 0 && recv_ts > send_ts {
712                return Some(recv_ts - send_ts);
713            }
714        }
715        None
716    }
717
718    #[cfg(not(feature = "timestamps"))]
719    #[inline]
720    fn compute_ttfb(&self, _send_ts: u64) -> Option<u64> {
721        None
722    }
723
724    /// Number of in-flight requests.
725    pub fn pending_count(&self) -> usize {
726        self.pending.len()
727    }
728
729    /// Flush before appending the next op if adding it would exceed
730    /// guard/iovec limits for scatter-gather sends. Also enforces the
731    /// `max_in_flight` cap so adversarial / stalled callers can't grow
732    /// the pending queue without bound.
733    fn pre_flush_if_needed(&mut self, has_guard: bool) -> Result<(), Error> {
734        if self.pending.len() >= self.max_in_flight {
735            return Err(Error::TooManyInFlight);
736        }
737        if self.buffered_ops == 0 {
738            return Ok(());
739        }
740        let next_guards = self.write_guards.len() + usize::from(has_guard);
741        let next_parts = 2 * next_guards + 1;
742        if next_guards > MAX_FLUSH_GUARDS || next_parts > MAX_FLUSH_IOVECS {
743            self.flush()?;
744        }
745        Ok(())
746    }
747
748    /// Flush after appending an op if we have reached `max_batch_size`.
749    fn post_flush_if_needed(&mut self) -> Result<(), Error> {
750        if self.buffered_ops >= self.max_batch_size {
751            self.flush()?;
752        }
753        Ok(())
754    }
755
756    /// Flush buffered `fire_*` commands as a single send.
757    ///
758    /// Called automatically by [`recv()`](Self::recv). Call explicitly if you
759    /// need commands to hit the wire before reading responses (e.g., when
760    /// interleaving fire/recv across multiple clients).
761    pub fn flush(&mut self) -> Result<(), Error> {
762        if self.write_buf.is_empty() && self.write_guards.is_empty() {
763            self.buffered_ops = 0;
764            return Ok(());
765        }
766
767        // Send the batch. If the send itself errors (peer RST, kernel
768        // ENOBUFS, etc.), discard the bytes-in-progress and the pending
769        // ops we'd push_back'd for them. Otherwise the next `flush()` would
770        // re-send the same buffer, and `recv()` would pop a `PendingOp` for
771        // a request that was never on the wire and then hang forever
772        // awaiting a response that will never come.
773        let send_outcome: Result<(), Error> = if self.write_guards.is_empty() {
774            self.conn
775                .send_nowait(&self.write_buf)
776                .map(|_| ())
777                .map_err(Error::from)
778        } else {
779            use ringline::SendPart;
780            let mut parts: Vec<SendPart<'_>> = Vec::with_capacity(2 * MAX_FLUSH_GUARDS + 1);
781            let mut pos = 0;
782            for (offset, guard) in self.write_guards.drain(..) {
783                if offset > pos {
784                    parts.push(SendPart::Copy(&self.write_buf[pos..offset]));
785                }
786                parts.push(SendPart::Guard(guard));
787                pos = offset;
788            }
789            if pos < self.write_buf.len() {
790                parts.push(SendPart::Copy(&self.write_buf[pos..]));
791            }
792            self.conn
793                .send_parts()
794                .submit_batch(parts)
795                .map(|_| ())
796                .map_err(Error::from)
797        };
798
799        if let Err(e) = send_outcome {
800            // Discard everything that was buffered for this flush. The
801            // pending ops that haven't been finalised yet (i.e., those
802            // beyond `flushed_count`) correspond to commands that were
803            // pushed onto `pending` but never reached the wire — drop
804            // them so `recv()` doesn't try to read responses for them.
805            self.write_buf.clear();
806            self.write_guards.clear();
807            self.buffered_ops = 0;
808            self.pending.truncate(self.flushed_count);
809            return Err(e);
810        }
811
812        // Only rewrite send timestamps when batching multiple ops —
813        // for a single buffered op the timestamp captured at fire time
814        // is already accurate.
815        if self.buffered_ops >= 1 {
816            let (send_ts, start) = self.timing_start();
817            for pending in self.pending.iter_mut().skip(self.flushed_count) {
818                pending.send_ts = send_ts;
819                pending.start = start;
820            }
821        }
822        self.flushed_count = self.pending.len();
823
824        self.write_buf.clear();
825        self.write_guards.clear();
826        self.buffered_ops = 0;
827
828        Ok(())
829    }
830
831    /// Fire a GET request without waiting for the response.
832    pub fn fire_get(&mut self, key: &[u8], user_data: u64) -> Result<(), Error> {
833        self.pre_flush_if_needed(false)?;
834        let tx_bytes;
835        if self.max_batch_size == 1 && self.write_guards.is_empty() && self.write_buf.is_empty() {
836            // Direct send — skip write_buf round-trip.
837            self.encode_buf.clear();
838            encode_request_into(&McRequest::get(key), &mut self.encode_buf)?;
839            tx_bytes = self.encode_buf.len() as u32;
840            self.conn.send_nowait(&self.encode_buf)?;
841        } else {
842            let before = self.write_buf.len();
843            encode_request_into(&McRequest::get(key), &mut self.write_buf)?;
844            tx_bytes = (self.write_buf.len() - before) as u32;
845            self.buffered_ops += 1;
846        }
847        let (send_ts, start) = self.timing_start_buffered();
848        self.pending.push_back(PendingOp {
849            kind: PendingOpKind::Get,
850            send_ts,
851            start,
852            user_data,
853            tx_bytes,
854        });
855        // Direct sends are already on the wire: keep flushed_count in
856        // sync so a later flush() failure only truncates pending ops
857        // that were never sent. Without this, the error path dropped
858        // an op whose response WILL arrive — permanently misattributing
859        // every subsequent response.
860        if self.max_batch_size == 1 && self.write_guards.is_empty() && self.write_buf.is_empty() {
861            self.flushed_count += 1;
862        }
863        self.post_flush_if_needed()?;
864        Ok(())
865    }
866
867    /// Fire a SET request (with copy) without waiting for the response.
868    pub fn fire_set(
869        &mut self,
870        key: &[u8],
871        value: &[u8],
872        flags: u32,
873        exptime: u32,
874        user_data: u64,
875    ) -> Result<(), Error> {
876        self.pre_flush_if_needed(false)?;
877        let req = McRequest::Set {
878            key,
879            value,
880            flags,
881            exptime,
882        };
883        let tx_bytes;
884        if self.max_batch_size == 1 && self.write_guards.is_empty() && self.write_buf.is_empty() {
885            // Direct send — skip write_buf round-trip.
886            self.encode_buf.clear();
887            encode_request_into(&req, &mut self.encode_buf)?;
888            tx_bytes = self.encode_buf.len() as u32;
889            self.conn.send_nowait(&self.encode_buf)?;
890        } else {
891            let before = self.write_buf.len();
892            encode_request_into(&req, &mut self.write_buf)?;
893            tx_bytes = (self.write_buf.len() - before) as u32;
894            self.buffered_ops += 1;
895        }
896        let (send_ts, start) = self.timing_start_buffered();
897        self.pending.push_back(PendingOp {
898            kind: PendingOpKind::Set,
899            send_ts,
900            start,
901            user_data,
902            tx_bytes,
903        });
904        // Direct sends are already on the wire: keep flushed_count in
905        // sync so a later flush() failure only truncates pending ops
906        // that were never sent. Without this, the error path dropped
907        // an op whose response WILL arrive — permanently misattributing
908        // every subsequent response.
909        if self.max_batch_size == 1 && self.write_guards.is_empty() && self.write_buf.is_empty() {
910            self.flushed_count += 1;
911        }
912        self.post_flush_if_needed()?;
913        Ok(())
914    }
915
916    /// Fire a SET request with zero-copy value via SendGuard.
917    ///
918    /// The guard value is kept alive and sent zero-copy at flush time via
919    /// scatter-gather I/O. The command prefix/suffix are buffered as copy data.
920    pub fn fire_set_with_guard<G: SendGuard>(
921        &mut self,
922        key: &[u8],
923        guard: G,
924        flags: u32,
925        exptime: u32,
926        user_data: u64,
927    ) -> Result<(), Error> {
928        let (ptr, value_len) = guard.as_ptr_len();
929        // Small guard values: fold into the coalescing copy path (`fire_set`)
930        // so they batch like plain SETs instead of taking the scatter-gather
931        // guard path, which flushes every few ops.
932        if self.zc_threshold != 0 && value_len < self.zc_threshold {
933            // SAFETY: `guard` is owned and live for the duration of this
934            // function, so `ptr`/`value_len` describe a valid byte range.
935            // `fire_set` copies those bytes into `write_buf`/`encode_buf`
936            // synchronously (no `.await` between this borrow and the copy),
937            // after which the guard `G` is dropped at the end of this call.
938            let value = unsafe { core::slice::from_raw_parts(ptr, value_len as usize) };
939            return self.fire_set(key, value, flags, exptime, user_data);
940        }
941        self.pre_flush_if_needed(true)?;
942        // Append prefix, record guard insertion point, append suffix —
943        // directly into write_buf, no intermediate allocation.
944        let before = self.write_buf.len();
945        append_set_guard_prefix(&mut self.write_buf, key, value_len as usize, flags, exptime)?;
946        self.write_guards
947            .push((self.write_buf.len(), GuardBox::new(guard)));
948        self.write_buf.extend_from_slice(b"\r\n");
949        let tx_bytes = (self.write_buf.len() - before + value_len as usize) as u32;
950        self.buffered_ops += 1;
951        let (send_ts, start) = self.timing_start_buffered();
952        self.pending.push_back(PendingOp {
953            kind: PendingOpKind::Set,
954            send_ts,
955            start,
956            user_data,
957            tx_bytes,
958        });
959        self.post_flush_if_needed()?;
960        Ok(())
961    }
962
963    /// Fire a DELETE request without waiting for the response.
964    pub fn fire_delete(&mut self, key: &[u8], user_data: u64) -> Result<(), Error> {
965        self.pre_flush_if_needed(false)?;
966        let tx_bytes;
967        if self.max_batch_size == 1 && self.write_guards.is_empty() && self.write_buf.is_empty() {
968            // Direct send — skip write_buf round-trip.
969            self.encode_buf.clear();
970            encode_request_into(&McRequest::delete(key), &mut self.encode_buf)?;
971            tx_bytes = self.encode_buf.len() as u32;
972            self.conn.send_nowait(&self.encode_buf)?;
973        } else {
974            let before = self.write_buf.len();
975            encode_request_into(&McRequest::delete(key), &mut self.write_buf)?;
976            tx_bytes = (self.write_buf.len() - before) as u32;
977            self.buffered_ops += 1;
978        }
979        let (send_ts, start) = self.timing_start_buffered();
980        self.pending.push_back(PendingOp {
981            kind: PendingOpKind::Delete,
982            send_ts,
983            start,
984            user_data,
985            tx_bytes,
986        });
987        // Direct sends are already on the wire: keep flushed_count in
988        // sync so a later flush() failure only truncates pending ops
989        // that were never sent. Without this, the error path dropped
990        // an op whose response WILL arrive — permanently misattributing
991        // every subsequent response.
992        if self.max_batch_size == 1 && self.write_guards.is_empty() && self.write_buf.is_empty() {
993            self.flushed_count += 1;
994        }
995        self.post_flush_if_needed()?;
996        Ok(())
997    }
998
999    /// Receive the next completed operation from the pipeline.
1000    ///
1001    /// Returns `Err(Error::NoPending)` if there are no in-flight requests.
1002    pub async fn recv(&mut self) -> Result<CompletedOp, Error> {
1003        // Flush any buffered fire_* commands before reading.
1004        self.flush()?;
1005
1006        let pending = self.pending.pop_front().ok_or(Error::NoPending)?;
1007        self.flushed_count = self.flushed_count.saturating_sub(1);
1008
1009        let response = match self.read_response().await {
1010            Ok(v) => v,
1011            Err(e) => {
1012                // Connection is broken — clear remaining pending ops so
1013                // subsequent recv() calls return NoPending instead of
1014                // reading stale/misaligned responses. Reset `flushed_count`
1015                // too — otherwise a stale count > 0 makes the next batched
1016                // `flush()` skip the send_ts rewrite for newly-buffered ops.
1017                self.pending.clear();
1018                self.flushed_count = 0;
1019                return Err(e);
1020            }
1021        };
1022        // TTFB from the kernel timestamp of the data that completed THIS
1023        // read. Sampling before the read used the PREVIOUS response's
1024        // arrival time for every op after the first in a pipelined batch.
1025        let ttfb_ns = self.compute_ttfb(pending.send_ts);
1026        let latency_ns = match pending.start {
1027            Some(start) => self.finish_timing(pending.send_ts, start),
1028            None => 0,
1029        };
1030        let rx_bytes = self.last_rx_bytes.get();
1031        let tx_bytes = pending.tx_bytes;
1032
1033        let op = match pending.kind {
1034            PendingOpKind::Get => {
1035                // Check for error responses first
1036                let result = match check_error_bytes(&response) {
1037                    Err(e) => Err(e),
1038                    Ok(()) => match response {
1039                        McResponseBytes::Values(mut values) => {
1040                            if values.is_empty() {
1041                                Ok(None)
1042                            } else {
1043                                let v = values.swap_remove(0);
1044                                Ok(Some(Value {
1045                                    data: v.data,
1046                                    flags: v.flags,
1047                                }))
1048                            }
1049                        }
1050                        _ => Err(Error::UnexpectedResponse),
1051                    },
1052                };
1053                let (success, hit) = match &result {
1054                    Ok(Some(_)) => (true, Some(true)),
1055                    Ok(None) => (true, Some(false)),
1056                    Err(_) => (false, None),
1057                };
1058                self.record(&CommandResult {
1059                    command: CommandType::Get,
1060                    latency_ns,
1061                    hit,
1062                    success,
1063                    ttfb_ns,
1064                    tx_bytes,
1065                    rx_bytes,
1066                });
1067                CompletedOp::Get {
1068                    result,
1069                    user_data: pending.user_data,
1070                    latency_ns,
1071                }
1072            }
1073            PendingOpKind::Set => {
1074                let result = match check_error_bytes(&response) {
1075                    Err(e) => Err(e),
1076                    Ok(()) => match response {
1077                        McResponseBytes::Stored => Ok(()),
1078                        _ => Err(Error::UnexpectedResponse),
1079                    },
1080                };
1081                self.record(&CommandResult {
1082                    command: CommandType::Set,
1083                    latency_ns,
1084                    hit: None,
1085                    success: result.is_ok(),
1086                    ttfb_ns,
1087                    tx_bytes,
1088                    rx_bytes,
1089                });
1090                CompletedOp::Set {
1091                    result,
1092                    user_data: pending.user_data,
1093                    latency_ns,
1094                }
1095            }
1096            PendingOpKind::Delete => {
1097                let result = match check_error_bytes(&response) {
1098                    Err(e) => Err(e),
1099                    Ok(()) => match response {
1100                        McResponseBytes::Deleted => Ok(true),
1101                        McResponseBytes::NotFound => Ok(false),
1102                        _ => Err(Error::UnexpectedResponse),
1103                    },
1104                };
1105                self.record(&CommandResult {
1106                    command: CommandType::Delete,
1107                    latency_ns,
1108                    hit: None,
1109                    success: result.is_ok(),
1110                    ttfb_ns,
1111                    tx_bytes,
1112                    rx_bytes,
1113                });
1114                CompletedOp::Delete {
1115                    result,
1116                    user_data: pending.user_data,
1117                    latency_ns,
1118                }
1119            }
1120        };
1121
1122        Ok(op)
1123    }
1124
1125    // ── Internal I/O (unchanged) ────────────────────────────────────────
1126
1127    /// Read and parse a single Memcache response from the connection.
1128    ///
1129    /// Uses zero-copy parsing via `with_bytes` + `ResponseBytes::parse`:
1130    /// value data are `Bytes::slice()` references into the accumulator's
1131    /// buffer rather than freshly allocated `Vec<u8>`.
1132    ///
1133    /// On `Error::Protocol` (parse failure) the underlying connection is
1134    /// closed: even though the parser advanced past the malformed bytes,
1135    /// the request/response framing is now irrecoverably misaligned and
1136    /// any further command would read garbage. Surfacing `Protocol` as a
1137    /// terminal error matches the recv() pending-queue clear and avoids
1138    /// silently desynced clients.
1139    pub(crate) async fn read_response(&self) -> Result<McResponseBytes, Error> {
1140        let mut result: Option<Result<McResponseBytes, Error>> = None;
1141        let n = self
1142            .conn
1143            .with_bytes(|bytes| {
1144                let len = bytes.len();
1145                match McResponseBytes::parse(bytes) {
1146                    Ok((response, consumed)) => {
1147                        result = Some(Ok(response));
1148                        ParseResult::Consumed(consumed)
1149                    }
1150                    Err(e) if e.is_incomplete() => ParseResult::Consumed(0),
1151                    Err(e) => {
1152                        result = Some(Err(Error::Protocol(e)));
1153                        ParseResult::Consumed(len)
1154                    }
1155                }
1156            })
1157            .await;
1158        self.last_rx_bytes.set(n as u32);
1159        if n == 0 {
1160            return result.unwrap_or(Err(Error::ConnectionClosed));
1161        }
1162        let r = result.unwrap();
1163        if matches!(r, Err(Error::Protocol(_))) {
1164            self.conn.close();
1165        }
1166        r
1167    }
1168
1169    /// Send an encoded command and read the response, converting error
1170    /// responses into `Error::Memcache`.
1171    async fn execute(&self, encoded: &[u8]) -> Result<McResponseBytes, Error> {
1172        self.conn.send(encoded)?;
1173        let response = self.read_response().await?;
1174        check_error_bytes(&response)?;
1175        Ok(response)
1176    }
1177
1178    // -- Commands (instrumented hot-path) ---------------------------------
1179
1180    /// Get the value of a key. Returns `None` on cache miss.
1181    pub async fn get(&mut self, key: impl AsRef<[u8]>) -> Result<Option<Value>, Error> {
1182        let key = key.as_ref();
1183        self.encode_buf.clear();
1184        encode_request_into(&McRequest::get(key), &mut self.encode_buf)?;
1185
1186        if !self.is_instrumented() {
1187            let response = self.execute(&self.encode_buf).await?;
1188            return match response {
1189                McResponseBytes::Values(mut values) => {
1190                    if values.is_empty() {
1191                        Ok(None)
1192                    } else {
1193                        let v = values.swap_remove(0);
1194                        Ok(Some(Value {
1195                            data: v.data,
1196                            flags: v.flags,
1197                        }))
1198                    }
1199                }
1200                _ => Err(Error::UnexpectedResponse),
1201            };
1202        }
1203
1204        let tx_bytes = self.encode_buf.len() as u32;
1205        let send_ts = self.send_timestamp();
1206        let start = Instant::now();
1207        let response = self.execute(&self.encode_buf).await;
1208        let latency_ns = self.finish_timing(send_ts, start);
1209        let rx_bytes = self.last_rx_bytes.get();
1210
1211        let result = match response {
1212            Ok(McResponseBytes::Values(mut values)) => {
1213                if values.is_empty() {
1214                    Ok(None)
1215                } else {
1216                    let v = values.swap_remove(0);
1217                    Ok(Some(Value {
1218                        data: v.data,
1219                        flags: v.flags,
1220                    }))
1221                }
1222            }
1223            Ok(_) => Err(Error::UnexpectedResponse),
1224            Err(e) => Err(e),
1225        };
1226
1227        let (success, hit) = match &result {
1228            Ok(Some(_)) => (true, Some(true)),
1229            Ok(None) => (true, Some(false)),
1230            Err(_) => (false, None),
1231        };
1232        self.record(&CommandResult {
1233            command: CommandType::Get,
1234            latency_ns,
1235            hit,
1236            success,
1237            ttfb_ns: None,
1238            tx_bytes,
1239            rx_bytes,
1240        });
1241        result
1242    }
1243
1244    /// Get values for multiple keys. Returns only hits, each with its key and CAS token.
1245    pub async fn gets(&mut self, keys: &[&[u8]]) -> Result<Vec<GetValue>, Error> {
1246        if keys.is_empty() {
1247            return Ok(Vec::new());
1248        }
1249        let encoded = encode_request(&McRequest::gets(keys))?;
1250        let response = self.execute(&encoded).await?;
1251        match response {
1252            McResponseBytes::Values(values) => Ok(values
1253                .into_iter()
1254                .map(|v| GetValue {
1255                    key: v.key,
1256                    data: v.data,
1257                    flags: v.flags,
1258                    cas: v.cas,
1259                })
1260                .collect()),
1261            _ => Err(Error::UnexpectedResponse),
1262        }
1263    }
1264
1265    /// Set a key-value pair with default flags (0) and no expiration.
1266    pub async fn set(
1267        &mut self,
1268        key: impl AsRef<[u8]>,
1269        value: impl AsRef<[u8]>,
1270    ) -> Result<(), Error> {
1271        self.set_with_options(key, value, 0, 0).await
1272    }
1273
1274    /// Set a key-value pair with custom flags and expiration time.
1275    pub async fn set_with_options(
1276        &mut self,
1277        key: impl AsRef<[u8]>,
1278        value: impl AsRef<[u8]>,
1279        flags: u32,
1280        exptime: u32,
1281    ) -> Result<(), Error> {
1282        let key = key.as_ref();
1283        let value = value.as_ref();
1284        self.encode_buf.clear();
1285        encode_request_into(
1286            &McRequest::Set {
1287                key,
1288                value,
1289                flags,
1290                exptime,
1291            },
1292            &mut self.encode_buf,
1293        )?;
1294
1295        if !self.is_instrumented() {
1296            let response = self.execute(&self.encode_buf).await?;
1297            return match response {
1298                McResponseBytes::Stored => Ok(()),
1299                _ => Err(Error::UnexpectedResponse),
1300            };
1301        }
1302
1303        let tx_bytes = self.encode_buf.len() as u32;
1304        let send_ts = self.send_timestamp();
1305        let start = Instant::now();
1306        let response = self.execute(&self.encode_buf).await;
1307        let latency_ns = self.finish_timing(send_ts, start);
1308        let rx_bytes = self.last_rx_bytes.get();
1309
1310        let result = match response {
1311            Ok(McResponseBytes::Stored) => Ok(()),
1312            Ok(_) => Err(Error::UnexpectedResponse),
1313            Err(e) => Err(e),
1314        };
1315        self.record(&CommandResult {
1316            command: CommandType::Set,
1317            latency_ns,
1318            hit: None,
1319            success: result.is_ok(),
1320            ttfb_ns: None,
1321            tx_bytes,
1322            rx_bytes,
1323        });
1324        result
1325    }
1326
1327    /// Store a key only if it does not already exist (ADD command).
1328    /// Returns `true` if stored, `false` if the key already exists.
1329    pub async fn add(
1330        &mut self,
1331        key: impl AsRef<[u8]>,
1332        value: impl AsRef<[u8]>,
1333    ) -> Result<bool, Error> {
1334        let key = key.as_ref();
1335        let value = value.as_ref();
1336        let encoded = encode_add(key, value)?;
1337        let response = self.execute(&encoded).await?;
1338        match response {
1339            McResponseBytes::Stored => Ok(true),
1340            McResponseBytes::NotStored => Ok(false),
1341            _ => Err(Error::UnexpectedResponse),
1342        }
1343    }
1344
1345    /// Store a key only if it already exists (REPLACE command).
1346    /// Returns `true` if stored, `false` if the key does not exist.
1347    pub async fn replace(
1348        &mut self,
1349        key: impl AsRef<[u8]>,
1350        value: impl AsRef<[u8]>,
1351    ) -> Result<bool, Error> {
1352        let key = key.as_ref();
1353        let value = value.as_ref();
1354        let encoded = encode_request(&McRequest::Replace {
1355            key,
1356            value,
1357            flags: 0,
1358            exptime: 0,
1359        })?;
1360        let response = self.execute(&encoded).await?;
1361        match response {
1362            McResponseBytes::Stored => Ok(true),
1363            McResponseBytes::NotStored => Ok(false),
1364            _ => Err(Error::UnexpectedResponse),
1365        }
1366    }
1367
1368    /// Increment a numeric value by delta. Returns the new value after incrementing.
1369    /// Returns `None` if the key does not exist.
1370    pub async fn incr(&mut self, key: impl AsRef<[u8]>, delta: u64) -> Result<Option<u64>, Error> {
1371        let key = key.as_ref();
1372        let encoded = encode_request(&McRequest::incr(key, delta))?;
1373        let response = self.execute(&encoded).await?;
1374        match response {
1375            McResponseBytes::Numeric(val) => Ok(Some(val)),
1376            McResponseBytes::NotFound => Ok(None),
1377            _ => Err(Error::UnexpectedResponse),
1378        }
1379    }
1380
1381    /// Decrement a numeric value by delta. Returns the new value after decrementing.
1382    /// Returns `None` if the key does not exist.
1383    pub async fn decr(&mut self, key: impl AsRef<[u8]>, delta: u64) -> Result<Option<u64>, Error> {
1384        let key = key.as_ref();
1385        let encoded = encode_request(&McRequest::decr(key, delta))?;
1386        let response = self.execute(&encoded).await?;
1387        match response {
1388            McResponseBytes::Numeric(val) => Ok(Some(val)),
1389            McResponseBytes::NotFound => Ok(None),
1390            _ => Err(Error::UnexpectedResponse),
1391        }
1392    }
1393
1394    /// Append data to an existing item's value.
1395    /// Returns `true` if stored, `false` if the key does not exist.
1396    pub async fn append(
1397        &mut self,
1398        key: impl AsRef<[u8]>,
1399        value: impl AsRef<[u8]>,
1400    ) -> Result<bool, Error> {
1401        let key = key.as_ref();
1402        let value = value.as_ref();
1403        let encoded = encode_request(&McRequest::append(key, value))?;
1404        let response = self.execute(&encoded).await?;
1405        match response {
1406            McResponseBytes::Stored => Ok(true),
1407            McResponseBytes::NotStored => Ok(false),
1408            _ => Err(Error::UnexpectedResponse),
1409        }
1410    }
1411
1412    /// Prepend data to an existing item's value.
1413    /// Returns `true` if stored, `false` if the key does not exist.
1414    pub async fn prepend(
1415        &mut self,
1416        key: impl AsRef<[u8]>,
1417        value: impl AsRef<[u8]>,
1418    ) -> Result<bool, Error> {
1419        let key = key.as_ref();
1420        let value = value.as_ref();
1421        let encoded = encode_request(&McRequest::prepend(key, value))?;
1422        let response = self.execute(&encoded).await?;
1423        match response {
1424            McResponseBytes::Stored => Ok(true),
1425            McResponseBytes::NotStored => Ok(false),
1426            _ => Err(Error::UnexpectedResponse),
1427        }
1428    }
1429
1430    /// Compare-and-swap: store the value only if the CAS token matches.
1431    /// Returns `Ok(true)` if stored, `Ok(false)` if the CAS token didn't match (EXISTS),
1432    /// or `Err` if the key was not found or another error occurred.
1433    pub async fn cas(
1434        &mut self,
1435        key: impl AsRef<[u8]>,
1436        value: impl AsRef<[u8]>,
1437        cas_unique: u64,
1438    ) -> Result<bool, Error> {
1439        let key = key.as_ref();
1440        let value = value.as_ref();
1441        let encoded = encode_request(&McRequest::cas(key, value, cas_unique))?;
1442        let response = self.execute(&encoded).await?;
1443        match response {
1444            McResponseBytes::Stored => Ok(true),
1445            McResponseBytes::Exists => Ok(false),
1446            McResponseBytes::NotFound => Err(Error::Memcache("NOT_FOUND".into())),
1447            _ => Err(Error::UnexpectedResponse),
1448        }
1449    }
1450
1451    /// Delete a key. Returns `true` if deleted, `false` if not found.
1452    pub async fn delete(&mut self, key: impl AsRef<[u8]>) -> Result<bool, Error> {
1453        let key = key.as_ref();
1454        self.encode_buf.clear();
1455        encode_request_into(&McRequest::delete(key), &mut self.encode_buf)?;
1456
1457        if !self.is_instrumented() {
1458            let response = self.execute(&self.encode_buf).await?;
1459            return match response {
1460                McResponseBytes::Deleted => Ok(true),
1461                McResponseBytes::NotFound => Ok(false),
1462                _ => Err(Error::UnexpectedResponse),
1463            };
1464        }
1465
1466        let tx_bytes = self.encode_buf.len() as u32;
1467        let send_ts = self.send_timestamp();
1468        let start = Instant::now();
1469        let response = self.execute(&self.encode_buf).await;
1470        let latency_ns = self.finish_timing(send_ts, start);
1471        let rx_bytes = self.last_rx_bytes.get();
1472
1473        let result = match response {
1474            Ok(McResponseBytes::Deleted) => Ok(true),
1475            Ok(McResponseBytes::NotFound) => Ok(false),
1476            Ok(_) => Err(Error::UnexpectedResponse),
1477            Err(e) => Err(e),
1478        };
1479        self.record(&CommandResult {
1480            command: CommandType::Delete,
1481            latency_ns,
1482            hit: None,
1483            success: result.is_ok(),
1484            ttfb_ns: None,
1485            tx_bytes,
1486            rx_bytes,
1487        });
1488        result
1489    }
1490
1491    /// Flush all items from the cache.
1492    pub async fn flush_all(&mut self) -> Result<(), Error> {
1493        let encoded = encode_request(&McRequest::flush_all())?;
1494        let response = self.execute(&encoded).await?;
1495        match response {
1496            McResponseBytes::Ok => Ok(()),
1497            _ => Err(Error::UnexpectedResponse),
1498        }
1499    }
1500
1501    /// Get the server version string.
1502    pub async fn version(&mut self) -> Result<Box<str>, Error> {
1503        let encoded = encode_request(&McRequest::version())?;
1504        let response = self.execute(&encoded).await?;
1505        match response {
1506            McResponseBytes::Version(v) => Ok(Box::from(String::from_utf8_lossy(v.as_ref()))),
1507            _ => Err(Error::UnexpectedResponse),
1508        }
1509    }
1510
1511    // -- Zero-copy SET -------------------------------------------------------
1512
1513    /// SET with zero-copy value via SendGuard. The guard pins value memory
1514    /// until the kernel completes the send.
1515    pub async fn set_with_guard<G: SendGuard>(
1516        &mut self,
1517        key: &[u8],
1518        guard: G,
1519        flags: u32,
1520        exptime: u32,
1521    ) -> Result<(), Error> {
1522        if !self.is_instrumented() {
1523            let (_, value_len) = guard.as_ptr_len();
1524            self.encode_buf.clear();
1525            append_set_guard_prefix(
1526                &mut self.encode_buf,
1527                key,
1528                value_len as usize,
1529                flags,
1530                exptime,
1531            )?;
1532
1533            let prefix: &[u8] = &self.encode_buf;
1534            self.conn.send_parts().build(move |b| {
1535                b.copy(prefix)
1536                    .guard(GuardBox::new(guard))
1537                    .copy(b"\r\n")
1538                    .submit()
1539            })?;
1540
1541            let response = self.read_response().await?;
1542            check_error_bytes(&response)?;
1543            return match response {
1544                McResponseBytes::Stored => Ok(()),
1545                _ => Err(Error::UnexpectedResponse),
1546            };
1547        }
1548
1549        let (_, value_len) = guard.as_ptr_len();
1550        self.encode_buf.clear();
1551        append_set_guard_prefix(
1552            &mut self.encode_buf,
1553            key,
1554            value_len as usize,
1555            flags,
1556            exptime,
1557        )?;
1558        let tx_bytes = (self.encode_buf.len() + value_len as usize + 2) as u32;
1559
1560        let send_ts = self.send_timestamp();
1561        let start = Instant::now();
1562
1563        let prefix: &[u8] = &self.encode_buf;
1564        self.conn.send_parts().build(move |b| {
1565            b.copy(prefix)
1566                .guard(GuardBox::new(guard))
1567                .copy(b"\r\n")
1568                .submit()
1569        })?;
1570
1571        let response = self.read_response().await;
1572        let latency_ns = self.finish_timing(send_ts, start);
1573        let rx_bytes = self.last_rx_bytes.get();
1574
1575        let result = match response {
1576            Ok(ref r) => {
1577                check_error_bytes(r)?;
1578                match r {
1579                    McResponseBytes::Stored => Ok(()),
1580                    _ => Err(Error::UnexpectedResponse),
1581                }
1582            }
1583            Err(e) => Err(e),
1584        };
1585        self.record(&CommandResult {
1586            command: CommandType::Set,
1587            latency_ns,
1588            hit: None,
1589            success: result.is_ok(),
1590            ttfb_ns: None,
1591            tx_bytes,
1592            rx_bytes,
1593        });
1594        result
1595    }
1596}
1597
1598// -- Zero-copy SET encoding helpers ------------------------------------------
1599
1600/// Append the memcache text SET prefix for guard-based sends to `buf`:
1601/// `set {key} {flags} {exptime} {valuelen}\r\n`.
1602/// The caller must append value bytes (via guard) + `\r\n` suffix.
1603///
1604/// Validates `key` (≤ [`MAX_KEY_LEN`]) and `value_len` (≤ [`MAX_VALUE_LEN`]);
1605/// returns the corresponding `Error` variant if either bound is exceeded so
1606/// that no bytes hit the wire for a request the server will reject. On error
1607/// nothing is appended to `buf`.
1608fn append_set_guard_prefix(
1609    buf: &mut Vec<u8>,
1610    key: &[u8],
1611    value_len: usize,
1612    flags: u32,
1613    exptime: u32,
1614) -> Result<(), Error> {
1615    validate_key(key)?;
1616    validate_value_len(value_len)?;
1617    let mut itoa_buf = itoa::Buffer::new();
1618    buf.extend_from_slice(b"set ");
1619    buf.extend_from_slice(key);
1620    buf.push(b' ');
1621    buf.extend_from_slice(itoa_buf.format(flags).as_bytes());
1622    buf.push(b' ');
1623    buf.extend_from_slice(itoa_buf.format(exptime).as_bytes());
1624    buf.push(b' ');
1625    buf.extend_from_slice(itoa_buf.format(value_len).as_bytes());
1626    buf.extend_from_slice(b"\r\n");
1627    Ok(())
1628}
1629
1630// -- Encoding helpers --------------------------------------------------------
1631
1632/// Reject requests whose key or value exceeds the protocol-defined caps.
1633fn validate_request(req: &McRequest<'_>) -> Result<(), Error> {
1634    match req {
1635        McRequest::Get { key }
1636        | McRequest::Incr { key, .. }
1637        | McRequest::Decr { key, .. }
1638        | McRequest::Delete { key } => validate_key(key),
1639        McRequest::Gets { keys } => {
1640            for k in keys.iter() {
1641                validate_key(k)?;
1642            }
1643            Ok(())
1644        }
1645        McRequest::Set { key, value, .. }
1646        | McRequest::Add { key, value, .. }
1647        | McRequest::Replace { key, value, .. }
1648        | McRequest::Append { key, value }
1649        | McRequest::Prepend { key, value }
1650        | McRequest::Cas { key, value, .. } => {
1651            validate_key(key)?;
1652            validate_value(value)
1653        }
1654        McRequest::FlushAll | McRequest::Version | McRequest::Quit => Ok(()),
1655    }
1656}
1657
1658/// Append the encoding of a `McRequest` to `buf`, rejecting oversized
1659/// keys/values up-front (see [`validate_request`]). No allocation once
1660/// `buf` has sufficient capacity. On error nothing is appended to `buf`.
1661pub(crate) fn encode_request_into(req: &McRequest<'_>, buf: &mut Vec<u8>) -> Result<(), Error> {
1662    validate_request(req)?;
1663    let size = match req {
1664        McRequest::Get { key } => 6 + key.len(),
1665        McRequest::Gets { keys } => 6 + keys.iter().map(|k| 1 + k.len()).sum::<usize>(),
1666        McRequest::Set { key, value, .. } | McRequest::Add { key, value, .. } => {
1667            41 + key.len() + value.len()
1668        }
1669        McRequest::Replace { key, value, .. } => 45 + key.len() + value.len(),
1670        McRequest::Incr { key, .. } | McRequest::Decr { key, .. } => 27 + key.len(),
1671        McRequest::Append { key, value } => 44 + key.len() + value.len(),
1672        McRequest::Prepend { key, value } => 45 + key.len() + value.len(),
1673        McRequest::Cas { key, value, .. } => 61 + key.len() + value.len(),
1674        McRequest::Delete { key } => 9 + key.len(),
1675        McRequest::FlushAll => 11,
1676        McRequest::Version => 9,
1677        McRequest::Quit => 6,
1678    };
1679    let start = buf.len();
1680    buf.resize(start + size, 0);
1681    let len = req.encode(&mut buf[start..]);
1682    buf.truncate(start + len);
1683    Ok(())
1684}
1685
1686/// Encode a `McRequest` into a freshly allocated `Vec<u8>`, rejecting
1687/// oversized keys/values up-front (see [`validate_request`]). Hot paths in
1688/// [`Client`] use [`encode_request_into`] with a reusable buffer instead.
1689pub(crate) fn encode_request(req: &McRequest<'_>) -> Result<Vec<u8>, Error> {
1690    let mut buf = Vec::new();
1691    encode_request_into(req, &mut buf)?;
1692    Ok(buf)
1693}
1694
1695/// Encode a SET command into a `Vec<u8>`. Test-only convenience; hot paths
1696/// use [`encode_request_into`] with a reused buffer.
1697#[cfg(test)]
1698pub(crate) fn encode_set(
1699    key: &[u8],
1700    value: &[u8],
1701    flags: u32,
1702    exptime: u32,
1703) -> Result<Vec<u8>, Error> {
1704    encode_request(&McRequest::Set {
1705        key,
1706        value,
1707        flags,
1708        exptime,
1709    })
1710}
1711
1712/// Encode an ADD command into a `Vec<u8>`.
1713pub(crate) fn encode_add(key: &[u8], value: &[u8]) -> Result<Vec<u8>, Error> {
1714    encode_request(&McRequest::Add {
1715        key,
1716        value,
1717        flags: 0,
1718        exptime: 0,
1719    })
1720}
1721
1722/// Check a `ResponseBytes` for error variants and return an appropriate `Error`.
1723pub(crate) fn check_error_bytes(response: &McResponseBytes) -> Result<(), Error> {
1724    match response {
1725        McResponseBytes::Error => Err(Error::Memcache("ERROR".into())),
1726        McResponseBytes::ClientError(msg) => Err(Error::Memcache(format!(
1727            "CLIENT_ERROR {}",
1728            String::from_utf8_lossy(msg)
1729        ))),
1730        McResponseBytes::ServerError(msg) => Err(Error::Memcache(format!(
1731            "SERVER_ERROR {}",
1732            String::from_utf8_lossy(msg)
1733        ))),
1734        _ => Ok(()),
1735    }
1736}
1737
1738// ── Kernel timestamp helper ─────────────────────────────────────────────
1739
1740#[cfg(feature = "timestamps")]
1741fn now_realtime_ns() -> u64 {
1742    let mut ts = libc::timespec {
1743        tv_sec: 0,
1744        tv_nsec: 0,
1745    };
1746    unsafe {
1747        libc::clock_gettime(libc::CLOCK_REALTIME, &mut ts);
1748    }
1749    ts.tv_sec as u64 * 1_000_000_000 + ts.tv_nsec as u64
1750}
1751
1752#[cfg(test)]
1753mod encode_tests {
1754    use super::*;
1755
1756    // 14-byte key with digits — exercises digit/key-byte adjacency and
1757    // multi-digit integer fields (itoa boundary cases).
1758    const KEY: &[u8] = b"user:123456789";
1759
1760    #[test]
1761    fn golden_encode_request_get() {
1762        let encoded = encode_request(&McRequest::get(KEY)).unwrap();
1763        assert_eq!(encoded, b"get user:123456789\r\n");
1764    }
1765
1766    #[test]
1767    fn golden_encode_request_delete() {
1768        let encoded = encode_request(&McRequest::delete(KEY)).unwrap();
1769        assert_eq!(encoded, b"delete user:123456789\r\n");
1770    }
1771
1772    #[test]
1773    fn golden_encode_request_set() {
1774        let encoded = encode_set(KEY, b"hello world value", 42, 7200).unwrap();
1775        assert_eq!(
1776            encoded,
1777            &b"set user:123456789 42 7200 17\r\nhello world value\r\n"[..]
1778        );
1779    }
1780
1781    #[test]
1782    fn golden_encode_request_set_zero_fields() {
1783        let encoded = encode_set(KEY, b"v", 0, 0).unwrap();
1784        assert_eq!(encoded, &b"set user:123456789 0 0 1\r\nv\r\n"[..]);
1785    }
1786
1787    #[test]
1788    fn golden_set_guard_prefix() {
1789        // Non-trivial flags / exptime / value_len so every integer field
1790        // is pinned byte-exactly.
1791        let mut prefix = Vec::new();
1792        append_set_guard_prefix(&mut prefix, KEY, 1024, 42, 7200).unwrap();
1793        assert_eq!(prefix, &b"set user:123456789 42 7200 1024\r\n"[..]);
1794    }
1795
1796    #[test]
1797    fn encode_into_appends_without_clobbering() {
1798        // The `_into` helper must append, never clear — clearing is the
1799        // caller's responsibility.
1800        let mut buf = b"EXISTING".to_vec();
1801        encode_request_into(&McRequest::get(KEY), &mut buf).unwrap();
1802        assert_eq!(buf, &b"EXISTINGget user:123456789\r\n"[..]);
1803    }
1804
1805    #[test]
1806    fn encode_into_error_leaves_buf_untouched() {
1807        let mut buf = b"EXISTING".to_vec();
1808        let long_key = vec![b'k'; MAX_KEY_LEN + 1];
1809        assert!(encode_request_into(&McRequest::get(&long_key), &mut buf).is_err());
1810        assert_eq!(buf, b"EXISTING");
1811        assert!(append_set_guard_prefix(&mut buf, &long_key, 16, 0, 0).is_err());
1812        assert_eq!(buf, b"EXISTING");
1813    }
1814
1815    // ── Coalescing byte-exactness ───────────────────────────────────────
1816    //
1817    // The write-coalescing layer (`max_batch_size > 1`) builds a single
1818    // `write_buf` by appending each `fire_*` command's encoding back-to-back,
1819    // then issues one send. The on-wire bytes for a coalesced batch must
1820    // therefore equal the exact concatenation of the N individual command
1821    // encodings. These tests pin that invariant without a live connection by
1822    // exercising the same `encode_request_into` append path `fire_*` uses.
1823
1824    #[test]
1825    fn coalesced_buf_equals_concatenation_of_individual_encodings() {
1826        // Build the batch the way the coalescing path does: append each
1827        // command into one shared buffer.
1828        let mut coalesced = Vec::new();
1829        encode_request_into(&McRequest::get(b"key1"), &mut coalesced).unwrap();
1830        encode_request_into(
1831            &McRequest::Set {
1832                key: b"key2",
1833                value: b"val",
1834                flags: 0,
1835                exptime: 0,
1836            },
1837            &mut coalesced,
1838        )
1839        .unwrap();
1840        encode_request_into(&McRequest::delete(b"key3"), &mut coalesced).unwrap();
1841
1842        // Build the reference: concatenate three independently-encoded
1843        // commands.
1844        let mut expected = Vec::new();
1845        expected.extend_from_slice(&encode_request(&McRequest::get(b"key1")).unwrap());
1846        expected.extend_from_slice(
1847            &encode_request(&McRequest::Set {
1848                key: b"key2",
1849                value: b"val",
1850                flags: 0,
1851                exptime: 0,
1852            })
1853            .unwrap(),
1854        );
1855        expected.extend_from_slice(&encode_request(&McRequest::delete(b"key3")).unwrap());
1856
1857        assert_eq!(coalesced, expected);
1858        // And the literal on-wire bytes, to catch framing regressions.
1859        assert_eq!(
1860            coalesced,
1861            &b"get key1\r\nset key2 0 0 3\r\nval\r\ndelete key3\r\n"[..]
1862        );
1863    }
1864
1865    #[test]
1866    fn coalesced_guard_set_byte_layout() {
1867        // Mirror the `fire_set_with_guard` buffer assembly: prefix into
1868        // write_buf, guard insertion point recorded, then "\r\n" suffix.
1869        // The flush scatter-gather then emits: prefix || value || "\r\n".
1870        let mut write_buf = Vec::new();
1871        let value: &[u8] = b"hello";
1872        append_set_guard_prefix(&mut write_buf, KEY, value.len(), 42, 7200).unwrap();
1873        let guard_offset = write_buf.len();
1874        write_buf.extend_from_slice(b"\r\n");
1875
1876        // Reconstruct the on-wire stream the flush path produces:
1877        // Copy(write_buf[..guard_offset]) Guard(value) Copy(write_buf[guard_offset..]).
1878        let mut wire = Vec::new();
1879        wire.extend_from_slice(&write_buf[..guard_offset]);
1880        wire.extend_from_slice(value);
1881        wire.extend_from_slice(&write_buf[guard_offset..]);
1882
1883        assert_eq!(
1884            wire,
1885            &b"set user:123456789 42 7200 5\r\nhello\r\n"[..],
1886            "coalesced guard SET must match a standard SET on the wire"
1887        );
1888
1889        // tx_bytes accounting: write_buf bytes + value bytes (guard is not
1890        // in write_buf). `fire_set_with_guard` computes
1891        // (write_buf.len() - before + value_len).
1892        assert_eq!(write_buf.len() + value.len(), wire.len());
1893    }
1894
1895    #[test]
1896    fn two_coalesced_guard_sets_interleave_correctly() {
1897        // Two guard SETs in one batch must produce
1898        // prefix1 value1 "\r\n" prefix2 value2 "\r\n" on the wire — this
1899        // pins the drain/interleave loop in `flush()`.
1900        let v1: &[u8] = b"aa";
1901        let v2: &[u8] = b"bbbb";
1902
1903        let mut write_buf = Vec::new();
1904        let mut guards: Vec<usize> = Vec::new();
1905
1906        append_set_guard_prefix(&mut write_buf, b"k1", v1.len(), 0, 0).unwrap();
1907        guards.push(write_buf.len());
1908        write_buf.extend_from_slice(b"\r\n");
1909
1910        append_set_guard_prefix(&mut write_buf, b"k2", v2.len(), 0, 0).unwrap();
1911        guards.push(write_buf.len());
1912        write_buf.extend_from_slice(b"\r\n");
1913
1914        // Replay the flush interleave loop.
1915        let values = [v1, v2];
1916        let mut wire = Vec::new();
1917        let mut pos = 0;
1918        for (i, &offset) in guards.iter().enumerate() {
1919            if offset > pos {
1920                wire.extend_from_slice(&write_buf[pos..offset]);
1921            }
1922            wire.extend_from_slice(values[i]);
1923            pos = offset;
1924        }
1925        if pos < write_buf.len() {
1926            wire.extend_from_slice(&write_buf[pos..]);
1927        }
1928
1929        assert_eq!(wire, &b"set k1 0 0 2\r\naa\r\nset k2 0 0 4\r\nbbbb\r\n"[..]);
1930    }
1931}
1932
1933#[cfg(test)]
1934mod tests {
1935    use super::*;
1936
1937    #[test]
1938    fn validate_key_accepts_max_len() {
1939        let key = vec![b'k'; MAX_KEY_LEN];
1940        assert!(validate_key(&key).is_ok());
1941    }
1942
1943    #[test]
1944    fn validate_key_rejects_oversized() {
1945        let key = vec![b'k'; MAX_KEY_LEN + 1];
1946        assert!(matches!(validate_key(&key), Err(Error::KeyTooLong)));
1947    }
1948
1949    #[test]
1950    fn validate_value_accepts_max_len() {
1951        let v = vec![0u8; MAX_VALUE_LEN];
1952        assert!(validate_value(&v).is_ok());
1953    }
1954
1955    #[test]
1956    fn validate_value_rejects_oversized() {
1957        let v = vec![0u8; MAX_VALUE_LEN + 1];
1958        assert!(matches!(validate_value(&v), Err(Error::ValueTooLong)));
1959    }
1960
1961    #[test]
1962    fn encode_request_get_rejects_long_key() {
1963        let key = vec![b'k'; MAX_KEY_LEN + 1];
1964        let r = encode_request(&McRequest::get(&key));
1965        assert!(matches!(r, Err(Error::KeyTooLong)));
1966    }
1967
1968    #[test]
1969    fn encode_request_set_rejects_long_value() {
1970        let key = b"k";
1971        let value = vec![0u8; MAX_VALUE_LEN + 1];
1972        let r = encode_request(&McRequest::Set {
1973            key,
1974            value: &value,
1975            flags: 0,
1976            exptime: 0,
1977        });
1978        assert!(matches!(r, Err(Error::ValueTooLong)));
1979    }
1980
1981    #[test]
1982    fn encode_request_gets_rejects_any_long_key() {
1983        let ok = vec![b'k'; MAX_KEY_LEN];
1984        let bad = vec![b'k'; MAX_KEY_LEN + 1];
1985        let keys: &[&[u8]] = &[&ok, &bad];
1986        let r = encode_request(&McRequest::gets(keys));
1987        assert!(matches!(r, Err(Error::KeyTooLong)));
1988    }
1989
1990    #[test]
1991    fn set_guard_prefix_rejects_long_key() {
1992        let key = vec![b'k'; MAX_KEY_LEN + 1];
1993        let r = append_set_guard_prefix(&mut Vec::new(), &key, 16, 0, 0);
1994        assert!(matches!(r, Err(Error::KeyTooLong)));
1995    }
1996
1997    #[test]
1998    fn set_guard_prefix_rejects_long_value_len() {
1999        let r = append_set_guard_prefix(&mut Vec::new(), b"k", MAX_VALUE_LEN + 1, 0, 0);
2000        assert!(matches!(r, Err(Error::ValueTooLong)));
2001    }
2002
2003    #[test]
2004    fn set_guard_prefix_accepts_max_value_len() {
2005        let r = append_set_guard_prefix(&mut Vec::new(), b"k", MAX_VALUE_LEN, 0, 0);
2006        assert!(r.is_ok());
2007    }
2008
2009    #[test]
2010    fn encode_request_passes_through_at_caps() {
2011        let key = vec![b'k'; MAX_KEY_LEN];
2012        let value = vec![0u8; MAX_VALUE_LEN];
2013        let r = encode_request(&McRequest::Set {
2014            key: &key,
2015            value: &value,
2016            flags: 0,
2017            exptime: 0,
2018        });
2019        assert!(r.is_ok());
2020        let buf = r.unwrap();
2021        // Sanity: encoded buffer starts with "set ".
2022        assert!(buf.starts_with(b"set "));
2023    }
2024}
2025
2026#[cfg(test)]
2027mod zc_threshold_tests {
2028    use super::*;
2029    use ringline::{ConnCtx, RegionId, SendGuard};
2030
2031    const KEY: &[u8] = b"user:123456789";
2032
2033    /// Heap-backed guard for tests. The `Vec` keeps bytes alive for the
2034    /// guard's lifetime.
2035    struct VecGuard(Vec<u8>);
2036
2037    impl SendGuard for VecGuard {
2038        fn as_ptr_len(&self) -> (*const u8, u32) {
2039            (self.0.as_ptr(), self.0.len() as u32)
2040        }
2041        fn region(&self) -> RegionId {
2042            RegionId::UNREGISTERED
2043        }
2044    }
2045
2046    /// A `Client` backed by a dangling `ConnCtx`. Only safe for tests that
2047    /// stay in the buffered path (no flush, no direct send, no recv).
2048    fn test_client(max_batch_size: usize, zc_threshold: u32) -> Client {
2049        let conn = ConnCtx::for_test(0, 0);
2050        Client::builder(conn)
2051            .max_batch_size(max_batch_size)
2052            .zc_threshold(zc_threshold)
2053            .build()
2054    }
2055
2056    #[test]
2057    fn small_guard_set_is_byte_identical_to_plain_set() {
2058        // Small guarded SET must produce byte-identical write_buf to a plain
2059        // copy SET of the same key/value/flags/exptime via fire_set.
2060        let value = vec![0xABu8; 64];
2061
2062        let mut guarded = test_client(4, 4096);
2063        guarded
2064            .fire_set_with_guard(KEY, VecGuard(value.clone()), 7, 42, 1)
2065            .unwrap();
2066
2067        let mut plain = test_client(4, 4096);
2068        plain.fire_set(KEY, &value, 7, 42, 1).unwrap();
2069
2070        assert_eq!(guarded.write_buf, plain.write_buf);
2071        assert!(guarded.write_guards.is_empty());
2072    }
2073
2074    #[test]
2075    fn small_value_takes_copy_path() {
2076        // 64B < 4096 → folds into copy path: write_guards empty, one buffered op.
2077        let mut client = test_client(4, 4096);
2078        client
2079            .fire_set_with_guard(KEY, VecGuard(vec![0u8; 64]), 0, 0, 1)
2080            .unwrap();
2081        assert!(client.write_guards.is_empty());
2082        assert_eq!(client.buffered_ops, 1);
2083        assert!(!client.write_buf.is_empty());
2084    }
2085
2086    #[test]
2087    fn large_value_takes_guard_path() {
2088        // 8KB >= 4096 → guard path: write_guards non-empty.
2089        let mut client = test_client(4, 4096);
2090        client
2091            .fire_set_with_guard(KEY, VecGuard(vec![0u8; 8192]), 0, 0, 1)
2092            .unwrap();
2093        assert_eq!(client.write_guards.len(), 1);
2094    }
2095
2096    #[test]
2097    fn threshold_zero_disables_fold() {
2098        // zc_threshold = 0 → always guard path, even for tiny values.
2099        let mut client = test_client(4, 0);
2100        client
2101            .fire_set_with_guard(KEY, VecGuard(vec![0u8; 64]), 0, 0, 1)
2102            .unwrap();
2103        assert_eq!(client.write_guards.len(), 1);
2104    }
2105
2106    #[test]
2107    fn guard_batch_accumulates_max_flush_guards_before_flush() {
2108        // zc_threshold = 0 forces the guard path. MAX_FLUSH_GUARDS guards
2109        // must accumulate without triggering a pre-flush: g guards need
2110        // 2g+1 iovecs, within MAX_FLUSH_IOVECS. A stale iovec cap of 8
2111        // used to force a flush at the 4th guard (batching only 3).
2112        let mut client = test_client(16, 0);
2113        for i in 0..MAX_FLUSH_GUARDS {
2114            client
2115                .fire_set_with_guard(KEY, VecGuard(vec![0u8; 64]), 0, 0, i as u64)
2116                .unwrap();
2117        }
2118        assert_eq!(
2119            client.write_guards.len(),
2120            MAX_FLUSH_GUARDS,
2121            "a full guard batch must accumulate without a premature flush"
2122        );
2123    }
2124
2125    #[test]
2126    fn batch_of_small_guard_sets_coalesces() {
2127        // N small guarded SETs at max_batch_size = N: all fold into the copy
2128        // path (write_guards stays empty), so they batch into a single buffer.
2129        // With a guard path they'd flush every MAX_FLUSH_GUARDS ops.
2130        const N: usize = 8;
2131        let mut client = test_client(N, 4096);
2132        for i in 0..N - 1 {
2133            client
2134                .fire_set_with_guard(KEY, VecGuard(vec![0u8; 64]), 0, 0, i as u64)
2135                .unwrap();
2136            assert!(
2137                client.write_guards.is_empty(),
2138                "op {i} must use the copy path, not the guard path"
2139            );
2140            assert_eq!(client.buffered_ops, i + 1);
2141        }
2142        assert_eq!(client.buffered_ops, N - 1);
2143        assert!(client.write_guards.is_empty());
2144    }
2145}