1pub mod actions;
15pub mod wire;
16
17pub use actions::MemcachedClientProtocol;
18
19use std::collections::VecDeque;
20use std::net::SocketAddr;
21use std::sync::Arc;
22
23use anyhow::{anyhow, Context, Result};
24use serde_json::json;
25use tokio::io::{AsyncReadExt, AsyncWriteExt};
26use tokio::net::TcpStream;
27use tokio::sync::{mpsc, oneshot};
28use tracing::{debug, error, info, warn};
29
30use crate::client::llm_budget::call_llm_for_client;
31use crate::client::memcached::actions::{
32 request_from_action, MEMCACHED_CONNECTED_EVENT, MEMCACHED_COUNTER_EVENT,
33 MEMCACHED_DELETED_EVENT, MEMCACHED_ERROR_EVENT, MEMCACHED_EXISTS_EVENT, MEMCACHED_MISS_EVENT,
34 MEMCACHED_NOT_FOUND_EVENT, MEMCACHED_NOT_STORED_EVENT, MEMCACHED_OK_EVENT,
35 MEMCACHED_STATS_EVENT, MEMCACHED_STORED_EVENT, MEMCACHED_TOUCHED_EVENT, MEMCACHED_VALUE_EVENT,
36 MEMCACHED_VERSION_EVENT, REQUEST_RESULT,
37};
38use crate::client::memcached::wire::{parse_reply, stats_json, Expect, Reply, Request};
39use crate::llm::actions::client_trait::{Client, ClientActionResult};
40use crate::llm::actions::protocol_trait::Protocol;
41use crate::llm::ollama_client::OllamaClient;
42use crate::protocol::Event;
43use crate::state::app_state::AppState;
44use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
45use crate::state::{AccessLogOwner, ClientId, ClientStatus};
46
47const TURN_QUEUE_CAPACITY: usize = 256;
50
51const MAX_IN_FLIGHT: usize = 128;
54
55const OUTBOUND_CAPACITY: usize = 64;
57
58enum Outbound {
60 Request {
61 request: Request,
62 ack: oneshot::Sender<Result<usize, String>>,
63 },
64 Disconnect {
65 ack: oneshot::Sender<()>,
66 },
67}
68
69enum Turn {
71 Connected,
72 Reply { expect: Expect, reply: Reply },
73}
74
75enum Applied {
77 Sent(usize),
78 Nothing,
79 Disconnect,
80}
81
82pub struct MemcachedClient;
83
84impl MemcachedClient {
85 pub async fn connect_with_llm_actions(
86 remote_addr: String,
87 llm_client: OllamaClient,
88 app_state: Arc<AppState>,
89 status_tx: mpsc::UnboundedSender<String>,
90 client_id: ClientId,
91 ) -> Result<SocketAddr> {
92 if remote_addr.trim().is_empty() {
94 return Err(anyhow!(
95 "memcached client needs a remote_addr (host:port); refusing to connect \
96 without one"
97 ));
98 }
99 let stream = TcpStream::connect(&remote_addr)
100 .await
101 .with_context(|| format!("Failed to connect to memcached at {remote_addr}"))?;
102 let local_addr = stream.local_addr()?;
103 let peer = stream.peer_addr()?;
104 info!("Memcached client {client_id} connected to {peer} (local {local_addr})");
105 app_state
106 .update_client_status(client_id, ClientStatus::Connected)
107 .await;
108 let _ = status_tx.send(format!("[CLIENT] Memcached client {client_id} connected"));
109 let _ = status_tx.send("__UPDATE_UI__".to_string());
110
111 let protocol = Arc::new(MemcachedClientProtocol::new());
112 let (outbound_tx, outbound_rx) = mpsc::channel::<Outbound>(OUTBOUND_CAPACITY);
113 let (turn_tx, turn_rx) = mpsc::channel::<Turn>(TURN_QUEUE_CAPACITY);
114
115 let command_rx =
118 crate::client::command_support::register_command_channel(&app_state, client_id).await;
119 app_state
120 .spawn_client_task(
121 client_id,
122 command_loop(
123 command_rx,
124 protocol.clone(),
125 outbound_tx.clone(),
126 client_id,
127 app_state.clone(),
128 status_tx.clone(),
129 ),
130 )
131 .await;
132
133 let turn_abort = app_state
134 .spawn_client_task(
135 client_id,
136 run_turns(
137 turn_rx,
138 outbound_tx,
139 protocol,
140 llm_client,
141 app_state.clone(),
142 status_tx.clone(),
143 client_id,
144 peer.to_string(),
145 ),
146 )
147 .await;
148
149 let _ = turn_tx.try_send(Turn::Connected);
150 app_state
151 .spawn_client_task(
152 client_id,
153 run_transport(
154 stream,
155 outbound_rx,
156 turn_tx,
157 app_state.clone(),
158 status_tx,
159 client_id,
160 turn_abort,
161 ),
162 )
163 .await;
164
165 Ok(local_addr)
166 }
167}
168
169async fn run_transport(
171 stream: TcpStream,
172 mut outbound_rx: mpsc::Receiver<Outbound>,
173 turn_tx: mpsc::Sender<Turn>,
174 app_state: Arc<AppState>,
175 status_tx: mpsc::UnboundedSender<String>,
176 client_id: ClientId,
177 turn_abort: tokio::task::AbortHandle,
178) {
179 let (mut reader, mut writer) = tokio::io::split(stream);
180 let mut pending: VecDeque<Expect> = VecDeque::new();
181 let mut buf: Vec<u8> = Vec::with_capacity(16 * 1024);
182 let mut chunk = vec![0u8; 16 * 1024];
183
184 let status = loop {
185 tokio::select! {
186 read = reader.read(&mut chunk) => {
187 let n = match read {
188 Ok(0) => {
189 info!("Memcached client {client_id} closed by server");
190 break ClientStatus::Disconnected;
191 }
192 Ok(n) => n,
193 Err(e) => {
194 error!("Memcached client {client_id} read error: {e}");
195 break ClientStatus::Error(e.to_string());
196 }
197 };
198 buf.extend_from_slice(&chunk[..n]);
199 if let Err(e) = drain_replies(&mut buf, &mut pending, &turn_tx, client_id) {
200 error!("Memcached client {client_id} protocol error: {e}");
201 break ClientStatus::Error(e);
202 }
203 }
204 out = outbound_rx.recv() => match out {
205 None => break ClientStatus::Disconnected,
206 Some(Outbound::Disconnect { ack }) => {
207 let _ = writer.shutdown().await;
208 let _ = ack.send(());
209 info!("Memcached client {client_id} disconnected on request");
210 break ClientStatus::Disconnected;
211 }
212 Some(Outbound::Request { request, ack }) => {
213 if pending.len() >= MAX_IN_FLIGHT {
214 let _ = ack.send(Err(format!(
215 "{MAX_IN_FLIGHT} requests are already waiting for a reply"
216 )));
217 continue;
218 }
219 let bytes = request.encode();
220 let written = async {
221 writer.write_all(&bytes).await?;
222 writer.flush().await
223 }
224 .await;
225 match written {
226 Ok(()) => {
227 debug!(
228 "Memcached client {client_id} sent {} ({} bytes)",
229 request.command(),
230 bytes.len()
231 );
232 pending.push_back(request.expect());
233 let _ = ack.send(Ok(bytes.len()));
234 }
235 Err(e) => {
236 let _ = ack.send(Err(format!("write failed: {e}")));
237 error!("Memcached client {client_id} write error: {e}");
238 break ClientStatus::Error(e.to_string());
239 }
240 }
241 }
242 }
243 }
244 };
245
246 turn_abort.abort();
247 app_state.remove_client_handle(client_id).await;
248 app_state.update_client_status(client_id, status).await;
249 let _ = status_tx.send(format!(
250 "[CLIENT] Memcached client {client_id} disconnected"
251 ));
252 let _ = status_tx.send("__UPDATE_UI__".to_string());
253}
254
255fn drain_replies(
257 buf: &mut Vec<u8>,
258 pending: &mut VecDeque<Expect>,
259 turn_tx: &mpsc::Sender<Turn>,
260 client_id: ClientId,
261) -> Result<(), String> {
262 while !buf.is_empty() {
263 let Some(expect) = pending.front() else {
264 return Err(format!(
265 "server sent {} octets with no request in flight",
266 buf.len()
267 ));
268 };
269 match parse_reply(buf, expect).map_err(|e| e.to_string())? {
270 None => return Ok(()),
271 Some((reply, used)) => {
272 buf.drain(..used);
273 let expect = pending.pop_front().expect("front() was Some");
274 if turn_tx.try_send(Turn::Reply { expect, reply }).is_err() {
275 warn!(
276 "Memcached client {client_id} dropped a reply: the model is \
277 {TURN_QUEUE_CAPACITY} replies behind decision=turn_queue_full"
278 );
279 }
280 }
281 }
282 }
283 Ok(())
284}
285
286fn events_for(expect: &Expect, reply: Reply) -> Vec<Event> {
288 use crate::utils::sanitize::line_field;
289 match (expect, reply) {
290 (Expect::Values { keys, .. }, Reply::Values { items, .. }) => keys
291 .iter()
292 .map(|key| match items.iter().find(|i| &i.key == key) {
293 None => Event::new(&MEMCACHED_MISS_EVENT, json!({ "key": key })),
294 Some(item) => match std::str::from_utf8(&item.data) {
295 Ok(text) => {
296 let mut data = json!({
297 "key": key,
298 "value": text,
299 "flags": item.flags,
300 });
301 if let Some(cas) = item.cas {
302 data["cas"] = json!(cas);
303 }
304 Event::new(&MEMCACHED_VALUE_EVENT, data)
305 }
306 Err(_) => Event::new(
307 &MEMCACHED_ERROR_EVENT,
308 json!({
309 "kind": "non_text_value",
310 "message": format!(
311 "the value stored under {key:?} is {} bytes that are not \
312 valid UTF-8; this client carries values as text only and \
313 does not re-encode them",
314 item.data.len()
315 ),
316 "command": "get",
317 "key": key,
318 }),
319 ),
320 },
321 })
322 .collect(),
323 (Expect::Stats { group }, Reply::Stats { entries }) => {
324 let mut data = json!({ "stats": stats_json(&entries) });
325 if let Some(g) = group {
326 data["group"] = json!(g);
327 }
328 vec![Event::new(&MEMCACHED_STATS_EVENT, data)]
329 }
330 (expect, Reply::Error { kind, message }) => {
331 let (command, key) = match expect {
332 Expect::Values { keys, with_cas } => (
333 if *with_cas { "gets" } else { "get" },
334 keys.first().cloned(),
335 ),
336 Expect::Status { command, key } => (*command, key.clone()),
337 Expect::Stats { .. } => ("stats", None),
338 };
339 let mut data = json!({
340 "kind": kind,
341 "message": line_field(&message),
342 "command": command,
343 });
344 if let Some(k) = key {
345 data["key"] = json!(k);
346 }
347 vec![Event::new(&MEMCACHED_ERROR_EVENT, data)]
348 }
349 (Expect::Status { command, key }, Reply::Status { line }) => {
350 let key = key.clone().unwrap_or_default();
351 let command = *command;
352 let event = match (command, line.as_str()) {
353 (_, "STORED") => Event::new(
354 &MEMCACHED_STORED_EVENT,
355 json!({"command": command, "key": key}),
356 ),
357 (_, "NOT_STORED") => Event::new(
358 &MEMCACHED_NOT_STORED_EVENT,
359 json!({"command": command, "key": key}),
360 ),
361 (_, "EXISTS") => Event::new(
362 &MEMCACHED_EXISTS_EVENT,
363 json!({"command": command, "key": key}),
364 ),
365 (_, "NOT_FOUND") => Event::new(
366 &MEMCACHED_NOT_FOUND_EVENT,
367 json!({"command": command, "key": key}),
368 ),
369 ("delete", "DELETED") => Event::new(&MEMCACHED_DELETED_EVENT, json!({"key": key})),
370 ("touch", "TOUCHED") => Event::new(&MEMCACHED_TOUCHED_EVENT, json!({"key": key})),
371 ("flush_all", "OK") => Event::new(&MEMCACHED_OK_EVENT, json!({"command": command})),
372 ("version", line) if line.starts_with("VERSION ") => Event::new(
373 &MEMCACHED_VERSION_EVENT,
374 json!({"version": line_field(&line["VERSION ".len()..])}),
375 ),
376 ("incr" | "decr", line) if line.parse::<u64>().is_ok() => Event::new(
377 &MEMCACHED_COUNTER_EVENT,
378 json!({
379 "command": command,
380 "key": key,
381 "value": line.parse::<u64>().unwrap_or_default(),
382 }),
383 ),
384 (_, line) => Event::new(
385 &MEMCACHED_ERROR_EVENT,
386 json!({
387 "kind": "unexpected_reply",
388 "message": format!(
389 "the server answered {:?}, which is not a reply to {command}",
390 line_field(line)
391 ),
392 "command": command,
393 "key": key,
394 }),
395 ),
396 };
397 vec![event]
398 }
399 (_, other) => {
401 warn!("Memcached reply {other:?} does not match its request {expect:?}");
402 Vec::new()
403 }
404 }
405}
406
407#[allow(clippy::too_many_arguments)]
409async fn run_turns(
410 mut turn_rx: mpsc::Receiver<Turn>,
411 outbound_tx: mpsc::Sender<Outbound>,
412 protocol: Arc<MemcachedClientProtocol>,
413 llm_client: OllamaClient,
414 app_state: Arc<AppState>,
415 status_tx: mpsc::UnboundedSender<String>,
416 client_id: ClientId,
417 remote_addr: String,
418) {
419 while let Some(turn) = turn_rx.recv().await {
420 let events = match turn {
421 Turn::Connected => vec![Event::new(
422 &MEMCACHED_CONNECTED_EVENT,
423 json!({ "remote_addr": remote_addr }),
424 )],
425 Turn::Reply { expect, reply } => events_for(&expect, reply),
426 };
427 for event in events {
428 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
429 continue;
430 };
431 let memory = app_state
432 .get_memory_for_client(client_id)
433 .await
434 .unwrap_or_default();
435 match call_llm_for_client(
436 &llm_client,
437 &app_state,
438 client_id.to_string(),
439 &instruction,
440 &memory,
441 Some(&event),
442 protocol.as_ref(),
443 &status_tx,
444 )
445 .await
446 {
447 Ok(result) => {
448 if let Some(mem) = result.memory_updates {
449 app_state.set_memory_for_client(client_id, mem).await;
450 }
451 info!(
452 "Memcached client {client_id} {} decision={} actions={}",
453 event.id(),
454 if result.actions.is_empty() {
455 "model_silent"
456 } else {
457 "model_actions"
458 },
459 result.actions.len()
460 );
461 for action in result.actions {
462 match apply_action(protocol.as_ref(), &outbound_tx, action).await {
463 Ok(Applied::Disconnect) => return,
464 Ok(_) => {}
465 Err(e) => {
466 error!("Memcached client {client_id} action failed: {e}");
467 let _ = status_tx.send(format!(
468 "[ERROR] Memcached client {client_id} action failed: {e}"
469 ));
470 }
471 }
472 }
473 }
474 Err(e) => {
475 error!(
476 "Memcached client {client_id} {} decision=llm_error: {e}",
477 event.id()
478 );
479 }
480 }
481 }
482 }
483}
484
485async fn apply_action(
488 protocol: &MemcachedClientProtocol,
489 outbound_tx: &mpsc::Sender<Outbound>,
490 action: serde_json::Value,
491) -> Result<Applied> {
492 match protocol.execute_action(action)? {
493 ClientActionResult::Custom { name, data } if name == REQUEST_RESULT => {
494 let request = request_from_action(&data)?
495 .ok_or_else(|| anyhow!("a request action produced no request"))?;
496 let (ack, done) = oneshot::channel();
497 outbound_tx
498 .send(Outbound::Request { request, ack })
499 .await
500 .map_err(|_| anyhow!("the connection is closed"))?;
501 let bytes = done
502 .await
503 .map_err(|_| anyhow!("the connection closed before the request was written"))?
504 .map_err(|e| anyhow!(e))?;
505 Ok(Applied::Sent(bytes))
506 }
507 ClientActionResult::Disconnect => {
508 let (ack, done) = oneshot::channel();
509 if outbound_tx.send(Outbound::Disconnect { ack }).await.is_ok() {
510 let _ = done.await;
511 }
512 Ok(Applied::Disconnect)
513 }
514 _ => Ok(Applied::Nothing),
515 }
516}
517
518async fn command_loop(
520 mut command_rx: mpsc::Receiver<ClientCommand>,
521 protocol: Arc<MemcachedClientProtocol>,
522 outbound_tx: mpsc::Sender<Outbound>,
523 client_id: ClientId,
524 app_state: Arc<AppState>,
525 status_tx: mpsc::UnboundedSender<String>,
526) {
527 while let Some(command) = command_rx.recv().await {
528 let action = command.action.clone();
529 let outcome = match apply_action(protocol.as_ref(), &outbound_tx, action.clone()).await {
530 Ok(Applied::Sent(bytes_sent)) => ClientSendOutcome::Sent { bytes_sent },
531 Ok(Applied::Nothing) => ClientSendOutcome::Executed {
532 detail: "executed (nothing to write)".to_string(),
533 },
534 Ok(Applied::Disconnect) => ClientSendOutcome::Disconnected,
535 Err(e) => ClientSendOutcome::Rejected {
536 error: e.to_string(),
537 },
538 };
539 app_state
540 .record_access_log(
541 AccessLogOwner::Client(client_id.as_u32()),
542 protocol.protocol_name(),
543 None,
544 "injected_action",
545 action,
546 vec![serde_json::to_value(&outcome).unwrap_or_default()],
547 )
548 .await;
549 let disconnect = matches!(outcome, ClientSendOutcome::Disconnected);
550 let _ = status_tx.send("__UPDATE_UI__".to_string());
551 crate::client::command_support::reply(command, Ok(outcome));
552 if disconnect {
553 break;
554 }
555 }
556}