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}