Skip to main content

netget/server/memcached/
mod.rs

1//! Memcached server (text protocol) — the model is the cache.
2//!
3//! **Nothing is stored here.** Grep this file for a `HashMap`, a `BTreeMap` or a file handle
4//! and you will not find one. Each command becomes an event, the model answers it, and the
5//! answer goes on the wire. Two `get`s of the same key are two independent questions.
6//!
7//! See `src/server/memcached/CLAUDE.md`.
8
9pub mod actions;
10pub mod protocol;
11
12pub use actions::MemcachedProtocol;
13
14use crate::llm::action_helper::call_llm;
15use crate::llm::ollama_client::OllamaClient;
16use crate::logging::emit::Log;
17use crate::protocol::{Event, SpawnContext};
18use crate::server::connection::ConnectionId;
19use crate::state::app_state::AppState;
20use crate::state::ServerId;
21use anyhow::{Context, Result};
22use std::net::SocketAddr;
23use std::sync::Arc;
24use tokio::io::{AsyncReadExt, AsyncWriteExt};
25use tokio::net::{TcpListener, TcpStream};
26use tokio::sync::mpsc;
27use tracing::{debug, warn};
28
29use actions::{
30    MEMCACHED_ARITHMETIC_EVENT, MEMCACHED_DELETE_EVENT, MEMCACHED_FLUSH_ALL_EVENT,
31    MEMCACHED_GET_EVENT, MEMCACHED_STATS_EVENT, MEMCACHED_STORE_EVENT, MEMCACHED_TOUCH_EVENT,
32    MEMCACHED_UNKNOWN_COMMAND_EVENT, MEMCACHED_VERSION_EVENT,
33};
34use protocol::{Command, Parsed};
35
36/// Ceiling on the read buffer for one still-incomplete command.
37///
38/// A storage command declares its own length, capped at `MAX_VALUE_LEN`; this leaves room
39/// for that plus the header and terminator, and refuses anything that would let a stalled
40/// client grow the buffer without bound.
41const MAX_BUFFERED: usize = protocol::MAX_VALUE_LEN + protocol::MAX_COMMAND_LINE + 16;
42
43/// How long an oversized storage command's data block is read and discarded after the
44/// refusal, before the socket is dropped. Bounded in time, and the bytes go into the reused
45/// read chunk, so the peer cannot turn the refusal into an allocation.
46const LINGER_AFTER_REFUSAL: std::time::Duration = std::time::Duration::from_secs(2);
47
48/// Read and discard until EOF, an error or [`LINGER_AFTER_REFUSAL`].
49async fn linger_discard<R: tokio::io::AsyncRead + Unpin>(reader: &mut R, chunk: &mut [u8]) {
50    let deadline = tokio::time::Instant::now() + LINGER_AFTER_REFUSAL;
51    while let Ok(Ok(n)) = tokio::time::timeout_at(deadline, reader.read(chunk)).await {
52        if n == 0 {
53            break;
54        }
55    }
56}
57
58/// How long to wait for the first byte of the first command from a peer that has only
59/// connected.
60///
61/// The memcached text protocol is client-speaks-first with no greeting, so a peer that has
62/// connected and sent nothing has made no claim on the server at all. Every real client —
63/// `libmemcached`, `pymemcache`, `memcached-tool`, a person on `nc` — issues a command
64/// immediately.
65const FIRST_COMMAND_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
66
67/// How long to wait for a *further* command once one has been answered.
68///
69/// Shorter than a session protocol's equivalent would be, because memcached has no session:
70/// every command is independent, the server holds nothing on a client's behalf, and a closed
71/// connection costs a pooling client one transparent reconnect and nothing else. Real
72/// memcached's own optional `-o idle_timeout` refuses to be set below 30 seconds, which is the
73/// protocol's own statement about how short is too short; two minutes is generous against that
74/// while still bounding an idle hold.
75const IDLE_BETWEEN_COMMANDS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(120);
76
77/// Concurrent connections this server admits.
78///
79/// [`crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS`]. Each connection may buffer up
80/// to [`MAX_BUFFERED`] bytes, so this is the multiplier that turns that per-connection bound
81/// into a total one.
82const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
83
84/// What a peer over [`MAX_CONNECTIONS`] is told before the socket closes.
85///
86/// `SERVER_ERROR <text>` is the text protocol's one way to say "the server failed on your
87/// request", and this file already uses it for the over-long-command case. No client can read
88/// it as a cache hit, a stored value or a successful delete, which is what makes it safe to
89/// send to a peer that has not spoken yet: the worst a client does is report a server error
90/// for its first command, which is exactly what happened.
91const CONNECTION_CAP_REFUSAL: &[u8] = b"SERVER_ERROR too many connections\r\n";
92
93/// What a storage command declaring more than [`protocol::MAX_VALUE_LEN`] bytes is told
94/// before the connection closes.
95///
96/// Upstream memcached's own text for the case, byte for byte (`SERVER_ERROR object too large
97/// for cache`), so a client that recognises it — libmemcached maps it to
98/// `MEMCACHED_E2BIG` — reports "value too large" rather than a generic server fault. The
99/// declared length is not echoed: it goes to the log.
100const VALUE_TOO_LARGE_REFUSAL: &str = "SERVER_ERROR object too large for cache\r\n";
101
102pub struct MemcachedServer;
103
104impl MemcachedServer {
105    /// Bind and serve. Returns `Err` if the listener cannot be bound, so `server_startup`
106    /// records `ServerStatus::Error` instead of a server that is not listening.
107    pub async fn spawn_with_llm_actions(ctx: SpawnContext) -> Result<SocketAddr> {
108        let listen_addr = ctx.legacy_listen_addr();
109        let SpawnContext {
110            llm_client,
111            state,
112            status_tx,
113            server_id,
114            ..
115        } = ctx;
116
117        let listener = TcpListener::bind(listen_addr)
118            .await
119            .with_context(|| format!("Memcached failed to bind {}", listen_addr))?;
120        let actual_addr = listener.local_addr()?;
121
122        Log::new(Some(&status_tx)).info(format!("Memcached server listening on {}", actual_addr));
123
124        let task_registrar = state.clone();
125        let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
126        let accept_handle = tokio::spawn(async move {
127            loop {
128                let (stream, peer_addr, permit) =
129                    match crate::server::accept_bounded::accept_bounded(
130                        &listener,
131                        &limiter,
132                        CONNECTION_CAP_REFUSAL,
133                        "Memcached",
134                        Some(&status_tx),
135                    )
136                    .await
137                    {
138                        Ok(triple) => triple,
139                        Err(e) => {
140                            Log::new(Some(&status_tx))
141                                .error(format!("Memcached accept error: {}", e));
142                            continue;
143                        }
144                    };
145
146                let connection_id = ConnectionId::new(state.get_next_unified_id().await);
147                let local_addr = stream.local_addr().unwrap_or(actual_addr);
148                Self::track_connection(&state, server_id, connection_id, peer_addr, local_addr)
149                    .await;
150
151                Log::new(Some(&status_tx)).info(format!(
152                    "Memcached connection {} from {}",
153                    connection_id, peer_addr
154                ));
155                let _ = status_tx.send("__UPDATE_UI__".to_string());
156
157                let llm = llm_client.clone();
158                let st = state.clone();
159                let tx = status_tx.clone();
160                let registrar = state.clone();
161                let conn_handle = tokio::spawn(async move {
162                    // Held for the life of the connection: releasing it here would cap the
163                    // accept rate rather than the number of live connections.
164                    let _permit = permit;
165                    if let Err(e) = Self::handle_connection(
166                        stream,
167                        peer_addr,
168                        connection_id,
169                        server_id,
170                        llm,
171                        st.clone(),
172                        tx,
173                    )
174                    .await
175                    {
176                        debug!("Memcached connection {} ended: {}", connection_id, e);
177                    }
178                    st.close_connection_on_server(server_id, connection_id)
179                        .await;
180                });
181
182                // Register the per-connection task too, not just the accept loop: aborting
183                // the accept loop releases the port but leaves every in-flight session
184                // running, so `stop_server` did not actually stop the server.
185                // `register_server_task` prunes finished handles on each call, so this cannot
186                // grow without bound.
187                registrar.register_server_task(server_id, conn_handle).await;
188            }
189        });
190
191        task_registrar
192            .register_server_task(server_id, accept_handle)
193            .await;
194
195        Ok(actual_addr)
196    }
197
198    async fn track_connection(
199        state: &Arc<AppState>,
200        server_id: ServerId,
201        connection_id: ConnectionId,
202        peer_addr: SocketAddr,
203        local_addr: SocketAddr,
204    ) {
205        use crate::state::server::{
206            ConnectionState as ServerConnectionState, ConnectionStatus, ProtocolConnectionInfo,
207        };
208        let now = crate::utils::clock::Instant::now();
209        state
210            .add_connection_to_server(
211                server_id,
212                ServerConnectionState {
213                    id: connection_id,
214                    remote_addr: peer_addr,
215                    local_addr,
216                    bytes_sent: 0,
217                    bytes_received: 0,
218                    packets_sent: 0,
219                    packets_received: 0,
220                    last_activity: now,
221                    status: ConnectionStatus::Active,
222                    status_changed_at: now,
223                    protocol_info: ProtocolConnectionInfo::empty(),
224                },
225            )
226            .await;
227    }
228
229    /// One command at a time, strictly sequentially.
230    ///
231    /// This is deliberately *not* the Idle/Processing/Accumulating state machine the
232    /// connection-oriented protocols hand-roll. That machine exists to stop two LLM calls
233    /// running on one connection; reading and answering in a single task achieves the same
234    /// thing by construction, and data arriving mid-call simply waits in the socket buffer.
235    #[allow(clippy::too_many_arguments)]
236    async fn handle_connection(
237        stream: TcpStream,
238        peer_addr: SocketAddr,
239        connection_id: ConnectionId,
240        server_id: ServerId,
241        llm_client: OllamaClient,
242        state: Arc<AppState>,
243        status_tx: mpsc::UnboundedSender<String>,
244    ) -> Result<()> {
245        // Split, never clone — the project rule for TcpStream. The write half is shared
246        // under one mutex between this reader task and the dashboard's peer-injection task,
247        // so their writes cannot interleave.
248        let (reader, write_half) = tokio::io::split(stream);
249        let write_half = Arc::new(tokio::sync::Mutex::new(write_half));
250        let handler = Arc::new(actions::MemcachedProtocol::new());
251
252        // Peer messaging: the dashboard's "message this peer" / "disconnect this peer"
253        // injects actions into THIS connection through the same executor the LLM path uses.
254        // Every memcached wire verb returns `ActionResult::Output` and
255        // `close_memcached_connection` returns `CloseConnection`, so the generic peer task
256        // covers the whole vocabulary — there is no `Custom` gap.
257        let peer_rx = crate::server::peer_support::register_peer_channel(
258            &state,
259            server_id,
260            connection_id.as_u32(),
261        )
262        .await;
263        crate::server::peer_support::spawn_peer_command_task(
264            peer_rx,
265            handler.clone(),
266            state.clone(),
267            server_id,
268            connection_id.as_u32(),
269            write_half.clone(),
270            status_tx.clone(),
271        );
272
273        let result = Self::run_connection(
274            reader,
275            &write_half,
276            &handler,
277            peer_addr,
278            connection_id,
279            server_id,
280            &llm_client,
281            &state,
282            &status_tx,
283        )
284        .await;
285
286        // Every exit path — EOF, read/write error, client quit, model-requested close,
287        // oversize buffer — lands here, so the handle never outlives the connection.
288        state
289            .remove_peer_handle(server_id, connection_id.as_u32())
290            .await;
291        result
292    }
293
294    /// The read/parse/answer/write loop, generic over the split halves so the peer task can
295    /// hold a clone of the write half. Setup and peer-handle teardown live in the caller.
296    #[allow(clippy::too_many_arguments)]
297    async fn run_connection<R, W>(
298        mut reader: R,
299        write_half: &Arc<tokio::sync::Mutex<W>>,
300        handler: &Arc<actions::MemcachedProtocol>,
301        peer_addr: SocketAddr,
302        connection_id: ConnectionId,
303        server_id: ServerId,
304        llm_client: &OllamaClient,
305        state: &Arc<AppState>,
306        status_tx: &mpsc::UnboundedSender<String>,
307    ) -> Result<()>
308    where
309        R: tokio::io::AsyncRead + Unpin,
310        W: tokio::io::AsyncWrite + Unpin,
311    {
312        let log = Log::new(Some(status_tx));
313
314        let mut buffer: Vec<u8> = Vec::with_capacity(4096);
315        let mut chunk = vec![0u8; 8192];
316        // False until this peer has sent something the parser recognised as a command, which
317        // is what separates "has connected and said nothing" from "is an established client".
318        let mut answered_one = false;
319
320        loop {
321            // Drain every complete command already buffered before reading more.
322            loop {
323                match protocol::parse_command(&buffer) {
324                    Parsed::Incomplete => break,
325                    Parsed::Invalid { message, consumed } => {
326                        let dropped = consumed.min(buffer.len());
327                        buffer.drain(..dropped);
328                        warn!(
329                            "Memcached {} sent an unusable command: {}",
330                            peer_addr, message
331                        );
332                        // Bytes the peer sent are bytes received, malformed or not. This arm
333                        // counted only what it wrote back, so a peer sending nothing but bad
334                        // commands showed `bytes_received == 0` on the dashboard.
335                        Self::count_received(state, server_id, connection_id, dropped).await;
336                        let line = format!("CLIENT_ERROR {}\r\n", message);
337                        {
338                            let mut writer = write_half.lock().await;
339                            writer.write_all(line.as_bytes()).await?;
340                            writer.flush().await?;
341                        }
342                        Self::count_sent(state, server_id, connection_id, line.len()).await;
343                    }
344                    Parsed::Fatal { message, too_large } => {
345                        // A storage command whose `<bytes>` is missing, unparseable or larger
346                        // than the cap. There is no boundary to skip to, so continuing would
347                        // mean parsing the peer's payload as commands. Reply and close.
348                        let received = buffer.len();
349                        buffer.clear();
350                        let line = if too_large {
351                            // The declared length alone decided this: not one octet of the
352                            // data block has been buffered. Swallowing it as upstream does
353                            // would mean reading up to 4 GiB purely to discard it, so the
354                            // connection closes instead. Fixed text, upstream's own: the
355                            // peer's number stays in the log.
356                            warn!(
357                                "Memcached {} decision=fail_closed_value_too_large: {}; the \
358                                 cap is {} bytes, closing before the data block is read",
359                                peer_addr,
360                                message,
361                                protocol::MAX_VALUE_LEN
362                            );
363                            VALUE_TOO_LARGE_REFUSAL.to_string()
364                        } else {
365                            warn!(
366                                "Memcached {} decision=connection_closed_unframable: {}; \
367                                 closing, because the data block's extent is unknown and \
368                                 skipping it would put the peer's payload on the command path",
369                                peer_addr, message
370                            );
371                            format!("CLIENT_ERROR {}\r\n", message)
372                        };
373                        Self::count_received(state, server_id, connection_id, received).await;
374                        {
375                            let mut writer = write_half.lock().await;
376                            writer.write_all(line.as_bytes()).await?;
377                            writer.flush().await?;
378                            let _ = writer.shutdown().await;
379                        }
380                        Self::count_sent(state, server_id, connection_id, line.len()).await;
381                        if too_large {
382                            // A real client sends the data block straight behind the header.
383                            // Closing with it unread makes the kernel answer with a reset,
384                            // which can destroy the refusal before the client reads it; so
385                            // the tail is read and discarded — never buffered — for a short,
386                            // fixed time first.
387                            linger_discard(&mut reader, &mut chunk).await;
388                        }
389                        return Ok(());
390                    }
391                    Parsed::Complete { command, consumed } => {
392                        buffer.drain(..consumed);
393                        answered_one = true;
394                        Self::count_received(state, server_id, connection_id, consumed).await;
395
396                        if matches!(command, Command::Quit) {
397                            debug!("Memcached {} sent quit", peer_addr);
398                            return Ok(());
399                        }
400
401                        let suppress = protocol::is_noreply(&command);
402                        let event = Self::event_for(&command);
403
404                        let outcome = call_llm(
405                            llm_client,
406                            state,
407                            server_id,
408                            Some(connection_id),
409                            &event,
410                            handler.as_ref(),
411                        )
412                        .await;
413
414                        let (reply, close) = match outcome {
415                            Ok(execution) => {
416                                for message in &execution.messages {
417                                    log.info(message);
418                                }
419                                let mut bytes = Vec::new();
420                                let mut close = false;
421                                // One command, one reply. The text protocol has no framing
422                                // between replies, so a second one is read by the client
423                                // as the answer to its *next* command. Only the chosen one
424                                // - the action the event's answer names, else the first - is
425                                // written.
426                                let chosen =
427                                    execution.chosen_reply(Self::preferred_actions(&command));
428                                let mut replies = 0usize;
429                                for (index, result) in execution.protocol_results.iter().enumerate()
430                                {
431                                    for (n, out) in result.get_all_output().iter().enumerate() {
432                                        replies += 1;
433                                        if Some(index) == chosen && n == 0 {
434                                            bytes.extend_from_slice(out);
435                                        }
436                                    }
437                                    close |= result.closes_connection();
438                                }
439                                if replies > 1 {
440                                    warn!(
441                                        "Memcached {} decision=duplicate_response_dropped \
442                                         command={}: {} extra repl{} not sent",
443                                        peer_addr,
444                                        Self::command_name(&command),
445                                        replies - 1,
446                                        if replies == 2 { "y" } else { "ies" }
447                                    );
448                                    log.warn(format!(
449                                        "Memcached {} decision=duplicate_response_dropped ({})",
450                                        peer_addr,
451                                        replies - 1
452                                    ));
453                                }
454                                if let Command::Retrieval { keys, .. } = &command {
455                                    if let Some((kept, dropped)) =
456                                        protocol::retain_requested_values(&bytes, keys)
457                                    {
458                                        if !dropped.is_empty() {
459                                            log.warn(format!(
460                                                "Memcached {} decision=unrequested_key_dropped: \
461                                                 the answer carried {:?}, which the client did \
462                                                 not ask for (asked: {:?})",
463                                                peer_addr, dropped, keys
464                                            ));
465                                            bytes = kept;
466                                        }
467                                    }
468                                }
469                                if bytes.is_empty() && !close {
470                                    // Nothing usable came back. Memcached clients block on a
471                                    // reply, so silence would hang them until their own
472                                    // timeout. SERVER_ERROR is the protocol's way of saying
473                                    // "this server failed" — never a fabricated cache hit.
474                                    // `decision=` tags mirror `src/server/radius/`: the text
475                                    // protocol has one failure reply, so on the wire a
476                                    // refusal, an outage and a model that answered with
477                                    // nothing usable are indistinguishable. Only the log can
478                                    // tell them apart.
479                                    warn!(
480                                        "Memcached {} decision=fail_closed_no_action command={}: \
481                                         no action produced",
482                                        peer_addr,
483                                        Self::command_name(&command)
484                                    );
485                                    bytes.extend_from_slice(
486                                        b"SERVER_ERROR no response was produced\r\n",
487                                    );
488                                }
489                                (bytes, close)
490                            }
491                            Err(e) => {
492                                // `SERVER_ERROR <text>` is the protocol's own way to say the
493                                // server failed on an otherwise valid command, and no client
494                                // treats it as data - so it can never be read as a cache hit,
495                                // a stored value or a successful delete.
496                                //
497                                // The reason is stripped of CR/LF: memcached replies are
498                                // CRLF-terminated with no length prefix, so a newline inside
499                                // the text would end the reply early and the rest would be
500                                // parsed as the next one.
501                                //
502                                // The overload/unavailable split that `redis` maps onto
503                                // `-LOADING` vs `-ERR` has nowhere to go here: the text
504                                // protocol defines exactly one server-failure reply, and
505                                // inventing a second would be a protocol extension no client
506                                // understands. It is carried in the log instead, so an
507                                // operator can still tell a saturated backend from a broken
508                                // one.
509                                let overloaded = crate::llm::is_overload_error(&e);
510                                let decision = if overloaded {
511                                    "fail_closed_llm_overloaded"
512                                } else {
513                                    "fail_closed_llm_error"
514                                };
515                                let text = crate::utils::WireFailure::classify(&e).prefixed_text();
516                                let reply = format!("SERVER_ERROR {text}\r\n");
517                                log.warn(format!(
518                                    "Memcached {} decision={} command={} replying {}: {}",
519                                    peer_addr,
520                                    decision,
521                                    Self::command_name(&command),
522                                    reply.trim(),
523                                    e
524                                ));
525                                (reply.into_bytes(), false)
526                            }
527                        };
528
529                        // `noreply` suppresses the reply for storage/delete/arithmetic
530                        // commands, per protocol.txt. The LLM call still happened; only the
531                        // write is skipped.
532                        if !suppress && !reply.is_empty() {
533                            log.trace(format!(
534                                "Memcached -> {}: {}",
535                                peer_addr,
536                                String::from_utf8_lossy(&reply)
537                            ));
538                            {
539                                let mut writer = write_half.lock().await;
540                                writer.write_all(&reply).await?;
541                                writer.flush().await?;
542                            }
543                            Self::count_sent(state, server_id, connection_id, reply.len()).await;
544                        }
545
546                        if close {
547                            debug!("Memcached closing {} on model request", peer_addr);
548                            return Ok(());
549                        }
550                    }
551                }
552            }
553
554            if buffer.len() > MAX_BUFFERED {
555                warn!(
556                    "Memcached {} decision=fail_closed_buffer_too_large: buffered {} bytes \
557                     without a complete command; closing",
558                    peer_addr,
559                    buffer.len()
560                );
561                let _ = write_half
562                    .lock()
563                    .await
564                    .write_all(b"SERVER_ERROR command too large\r\n")
565                    .await;
566                return Ok(());
567            }
568
569            // The deadline wraps this `read()` and nothing else in the loop. Everything that
570            // can legitimately take minutes — the LLM round-trip, and a `manual` rule parking
571            // the command for a human to answer (`src/state/intercepts.rs`, 300s by default) —
572            // happens above, after a read has already returned. So a slow answer can never be
573            // timed out from under itself; only a peer holding the connection while sending
574            // nothing is.
575            let read_timeout = if answered_one {
576                IDLE_BETWEEN_COMMANDS_TIMEOUT
577            } else {
578                FIRST_COMMAND_READ_TIMEOUT
579            };
580            let n = match tokio::time::timeout(read_timeout, reader.read(&mut chunk)).await {
581                Ok(read) => read?,
582                Err(_) => {
583                    debug!(
584                        "Memcached {} sent nothing for {}s; closing idle connection",
585                        peer_addr,
586                        read_timeout.as_secs()
587                    );
588                    return Ok(());
589                }
590            };
591            if n == 0 {
592                debug!("Memcached {} closed the connection", peer_addr);
593                return Ok(());
594            }
595            buffer.extend_from_slice(&chunk[..n]);
596        }
597    }
598
599    async fn count_received(
600        state: &Arc<AppState>,
601        server_id: ServerId,
602        connection_id: ConnectionId,
603        bytes: usize,
604    ) {
605        state
606            .update_connection_stats(
607                server_id,
608                connection_id,
609                Some(bytes as u64),
610                None,
611                Some(1),
612                None,
613            )
614            .await;
615    }
616
617    async fn count_sent(
618        state: &Arc<AppState>,
619        server_id: ServerId,
620        connection_id: ConnectionId,
621        bytes: usize,
622    ) {
623        state
624            .update_connection_stats(
625                server_id,
626                connection_id,
627                None,
628                Some(bytes as u64),
629                None,
630                Some(1),
631            )
632            .await;
633    }
634
635    /// The actions whose reply wins when a batch answers one command with several; see
636    /// `ExecutionResult::chosen_reply`.
637    fn preferred_actions(command: &Command) -> &'static [&'static str] {
638        match command {
639            Command::Retrieval { .. } => &["send_memcached_values"],
640            Command::Stats { .. } => &["send_memcached_stats"],
641            Command::Version => &["send_memcached_version"],
642            Command::Arithmetic { .. } => &["send_memcached_number"],
643            _ => &[],
644        }
645    }
646
647    fn command_name(command: &Command) -> &'static str {
648        match command {
649            Command::Retrieval { command, .. } => command,
650            Command::Storage { command, .. } => command,
651            Command::Delete { .. } => "delete",
652            Command::Arithmetic { command, .. } => command,
653            Command::Touch { .. } => "touch",
654            Command::Stats { .. } => "stats",
655            Command::Version => "version",
656            Command::FlushAll { .. } => "flush_all",
657            Command::Quit => "quit",
658            Command::Unknown { .. } => "unknown",
659        }
660    }
661
662    /// Turn a parsed command into the event the model sees. Structured fields only; the one
663    /// place bytes appear is a storage command's data block, which carries an explicit
664    /// `value_encoding` saying whether it is text or hex.
665    fn event_for(command: &Command) -> Event {
666        match command {
667            Command::Retrieval { command, keys } => Event::new(
668                &MEMCACHED_GET_EVENT,
669                serde_json::json!({
670                    "command": command,
671                    "keys": keys,
672                    "answer_with": format!(
673                        "send_memcached_values with one entry for each of {:?} that the cache \
674                         holds, and none for a key it does not hold; an empty values list is \
675                         a miss. Never include a key that is not in that list",
676                        keys
677                    ),
678                }),
679            ),
680            Command::Storage {
681                command,
682                key,
683                flags,
684                exptime,
685                bytes,
686                cas_unique,
687                data,
688                ..
689            } => {
690                let (value, encoding) = match std::str::from_utf8(data) {
691                    Ok(s) => (s.to_string(), "utf8"),
692                    Err(_) => (hex::encode(data), "hex"),
693                };
694                Event::new(
695                    &MEMCACHED_STORE_EVENT,
696                    serde_json::json!({
697                        "command": command,
698                        "key": key,
699                        "flags": flags,
700                        "exptime": exptime,
701                        "bytes": bytes,
702                        "cas_unique": cas_unique,
703                        "value": value,
704                        "value_encoding": encoding,
705                    }),
706                )
707            }
708            Command::Delete { key, .. } => {
709                Event::new(&MEMCACHED_DELETE_EVENT, serde_json::json!({ "key": key }))
710            }
711            Command::Arithmetic {
712                command,
713                key,
714                delta,
715                ..
716            } => Event::new(
717                &MEMCACHED_ARITHMETIC_EVENT,
718                serde_json::json!({ "command": command, "key": key, "delta": delta }),
719            ),
720            Command::Touch { key, exptime, .. } => Event::new(
721                &MEMCACHED_TOUCH_EVENT,
722                serde_json::json!({ "key": key, "exptime": exptime }),
723            ),
724            Command::Stats { argument } => Event::new(
725                &MEMCACHED_STATS_EVENT,
726                serde_json::json!({
727                    "argument": argument,
728                    "answer_with": "send_memcached_stats with a stats object under memcached's \
729                                    own stat names - version, curr_items, total_items, bytes, \
730                                    uptime, pid, curr_connections - holding what your \
731                                    instruction says (a version it names goes in version, an \
732                                    item count in curr_items)",
733                }),
734            ),
735            Command::Version => Event::new(&MEMCACHED_VERSION_EVENT, serde_json::json!({})),
736            Command::FlushAll { delay, .. } => Event::new(
737                &MEMCACHED_FLUSH_ALL_EVENT,
738                serde_json::json!({ "delay": delay }),
739            ),
740            Command::Unknown { line } => Event::new(
741                &MEMCACHED_UNKNOWN_COMMAND_EVENT,
742                serde_json::json!({ "line": line }),
743            ),
744            Command::Quit => {
745                // Handled before this function is reached; quit is answered by closing.
746                Event::new(
747                    &MEMCACHED_UNKNOWN_COMMAND_EVENT,
748                    serde_json::json!({ "line": "quit" }),
749                )
750            }
751        }
752    }
753}