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}