1use crate::client::memcached::wire::{
14 validate_key, validate_stats_group, Request, StoreVerb, MAX_GET_KEYS,
15};
16use crate::llm::actions::{
17 client_trait::{Client, ClientActionResult},
18 protocol_trait::Protocol,
19 ActionDefinition, Parameter,
20};
21use crate::protocol::log_template::LogTemplate;
22use crate::protocol::EventType;
23use crate::server::memcached::protocol::MAX_VALUE_LEN;
24use crate::state::app_state::AppState;
25use anyhow::{anyhow, Context, Result};
26use serde_json::{json, Value};
27use std::sync::LazyLock;
28
29pub const REQUEST_RESULT: &str = "memcached_request";
31
32fn param(name: &str, type_hint: &str, description: &str, required: bool) -> Parameter {
33 Parameter {
34 name: name.to_string(),
35 type_hint: type_hint.to_string(),
36 description: description.to_string(),
37 required,
38 }
39}
40
41fn key_param(desc: &str) -> Parameter {
42 param("key", "string", desc, true)
43}
44
45fn command_param() -> Parameter {
46 param(
47 "command",
48 "string",
49 "The request this answers: set, add, replace, append, prepend, cas, delete, incr, \
50 decr or touch",
51 true,
52 )
53}
54
55pub static MEMCACHED_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
56 EventType::new(
57 "memcached_connected",
58 "Connected to a memcached server over TCP",
59 json!({"type": "memcached_get", "keys": ["greeting"]}),
60 )
61 .with_parameters(vec![param(
62 "remote_addr",
63 "string",
64 "The server this client is connected to",
65 true,
66 )])
67});
68
69pub static MEMCACHED_VALUE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
70 EventType::new(
71 "memcached_value",
72 "A requested key was found: one event per hit of a get/gets",
73 json!({"type": "memcached_set", "key": "seen", "value": "yes"}),
74 )
75 .with_parameters(vec![
76 key_param("The key the request named"),
77 param("value", "string", "The stored value, as text", true),
78 param(
79 "flags",
80 "number",
81 "The 32-bit flags stored with the item",
82 true,
83 ),
84 param(
85 "cas",
86 "number",
87 "The CAS unique (gets only); pass it to memcached_cas to update the item only if \
88 nobody else has",
89 false,
90 ),
91 ])
92});
93
94pub static MEMCACHED_MISS_EVENT: LazyLock<EventType> = LazyLock::new(|| {
95 EventType::new(
96 "memcached_miss",
97 "A requested key is not on the server: one event per miss of a get/gets",
98 json!({"type": "memcached_add", "key": "greeting", "value": "hello"}),
99 )
100 .with_parameters(vec![key_param("The key that was not found")])
101});
102
103pub static MEMCACHED_STORED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
104 EventType::new(
105 "memcached_stored",
106 "The server stored the value (STORED)",
107 json!({"type": "memcached_get", "keys": ["greeting"]}),
108 )
109 .with_parameters(vec![command_param(), key_param("The key that was stored")])
110});
111
112pub static MEMCACHED_NOT_STORED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
113 EventType::new(
114 "memcached_not_stored",
115 "The server did not store the value because the add/replace/append/prepend \
116 condition did not hold (NOT_STORED)",
117 json!({"type": "memcached_set", "key": "greeting", "value": "hello"}),
118 )
119 .with_parameters(vec![
120 command_param(),
121 key_param("The key the request named"),
122 ])
123});
124
125pub static MEMCACHED_EXISTS_EVENT: LazyLock<EventType> = LazyLock::new(|| {
126 EventType::new(
127 "memcached_exists",
128 "A cas was refused because the item changed since its CAS unique was read (EXISTS)",
129 json!({"type": "memcached_gets", "keys": ["counter"]}),
130 )
131 .with_parameters(vec![
132 command_param(),
133 key_param("The key the request named"),
134 ])
135});
136
137pub static MEMCACHED_NOT_FOUND_EVENT: LazyLock<EventType> = LazyLock::new(|| {
138 EventType::new(
139 "memcached_not_found",
140 "The key the request named does not exist (NOT_FOUND)",
141 json!({"type": "memcached_set", "key": "counter", "value": "0"}),
142 )
143 .with_parameters(vec![
144 command_param(),
145 key_param("The key the request named"),
146 ])
147});
148
149pub static MEMCACHED_DELETED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
150 EventType::new(
151 "memcached_deleted",
152 "The key was deleted (DELETED)",
153 json!({"type": "memcached_get", "keys": ["greeting"]}),
154 )
155 .with_parameters(vec![key_param("The key that was deleted")])
156});
157
158pub static MEMCACHED_TOUCHED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
159 EventType::new(
160 "memcached_touched",
161 "The key's expiration time was updated (TOUCHED)",
162 json!({"type": "memcached_get", "keys": ["session"]}),
163 )
164 .with_parameters(vec![key_param("The key that was touched")])
165});
166
167pub static MEMCACHED_COUNTER_EVENT: LazyLock<EventType> = LazyLock::new(|| {
168 EventType::new(
169 "memcached_counter",
170 "The new value of a counter after incr/decr",
171 json!({"type": "memcached_incr", "key": "hits", "delta": 1}),
172 )
173 .with_parameters(vec![
174 param(
175 "command",
176 "string",
177 "The request answered: incr or decr",
178 true,
179 ),
180 key_param("The counter's key"),
181 param(
182 "value",
183 "number",
184 "The counter's value after the change",
185 true,
186 ),
187 ])
188});
189
190pub static MEMCACHED_STATS_EVENT: LazyLock<EventType> = LazyLock::new(|| {
191 EventType::new(
192 "memcached_stats",
193 "The server's statistics, as one object of name to value",
194 json!({"type": "disconnect"}),
195 )
196 .with_parameters(vec![
197 param(
198 "stats",
199 "object",
200 "Every STAT line, name to value (values are strings, as the server wrote them)",
201 true,
202 ),
203 param(
204 "group",
205 "string",
206 "The stats group asked for, if any",
207 false,
208 ),
209 ])
210});
211
212pub static MEMCACHED_VERSION_EVENT: LazyLock<EventType> = LazyLock::new(|| {
213 EventType::new(
214 "memcached_version",
215 "The server's version string",
216 json!({"type": "memcached_stats"}),
217 )
218 .with_parameters(vec![param(
219 "version",
220 "string",
221 "The version, e.g. 1.6.45",
222 true,
223 )])
224});
225
226pub static MEMCACHED_OK_EVENT: LazyLock<EventType> = LazyLock::new(|| {
227 EventType::new(
228 "memcached_ok",
229 "The server acknowledged a flush_all (OK)",
230 json!({"type": "memcached_get", "keys": ["greeting"]}),
231 )
232 .with_parameters(vec![param(
233 "command",
234 "string",
235 "The request acknowledged: flush_all",
236 true,
237 )])
238});
239
240pub static MEMCACHED_ERROR_EVENT: LazyLock<EventType> =
241 LazyLock::new(|| {
242 EventType::new(
243 "memcached_error",
244 "A request failed: the server answered ERROR, CLIENT_ERROR or SERVER_ERROR, or a value \
245 it returned is not text",
246 json!({"type": "memcached_version"}),
247 )
248 .with_parameters(vec![
249 param(
250 "kind",
251 "string",
252 "error (unknown command), client_error (the request was malformed), server_error \
253 (the server could not do it, e.g. out of memory), or non_text_value (a stored \
254 value is not UTF-8 and this client carries text only)",
255 true,
256 ),
257 param("message", "string", "What the server or the client said", true),
258 param("command", "string", "The request this answers", false),
259 param("key", "string", "The key involved, where there is one", false),
260 ])
261 });
262
263pub struct MemcachedClientProtocol;
265
266impl MemcachedClientProtocol {
267 pub fn new() -> Self {
268 Self
269 }
270}
271
272impl Default for MemcachedClientProtocol {
273 fn default() -> Self {
274 Self::new()
275 }
276}
277
278fn store_action(verb: &str, description: &str, cas: bool) -> ActionDefinition {
279 let mut parameters = vec![
280 key_param("Key, at most 250 bytes, no spaces or control characters"),
281 param(
282 "value",
283 "string",
284 "The value, as text. Any characters, including spaces and newlines",
285 true,
286 ),
287 param(
288 "flags",
289 "number",
290 "32-bit opaque flags stored with the item (default 0)",
291 false,
292 ),
293 param(
294 "exptime",
295 "number",
296 "Expiration: 0 = never (default), seconds from now up to 30 days, or a Unix \
297 timestamp",
298 false,
299 ),
300 ];
301 let mut example = json!({
302 "type": format!("memcached_{verb}"),
303 "key": "greeting",
304 "value": "hello world",
305 });
306 if cas {
307 parameters.push(param(
308 "cas_unique",
309 "number",
310 "The CAS unique a memcached_gets returned for this key",
311 true,
312 ));
313 example["cas_unique"] = json!(12);
314 }
315 let cas_part = if cas { " cas={cas_unique}" } else { "" };
316 ActionDefinition {
317 name: format!("memcached_{verb}"),
318 description: description.to_string(),
319 parameters,
320 example,
321 log_template: Some(
322 LogTemplate::new()
323 .with_info(format!("-> memcached {verb} {{key}}{cas_part}"))
324 .with_debug(format!(
325 "memcached {verb}: key={{key}} flags={{flags}} exptime={{exptime}}{cas_part} \
326 value={{preview(value,80)}}"
327 )),
328 ),
329 }
330}
331
332fn simple_action(
333 name: &str,
334 description: &str,
335 parameters: Vec<Parameter>,
336 example: Value,
337) -> ActionDefinition {
338 let info = match name {
339 "memcached_get" => "-> memcached get {json(keys)}",
340 "memcached_gets" => "-> memcached gets {json(keys)}",
341 "memcached_delete" => "-> memcached delete {key}",
342 "memcached_incr" => "-> memcached incr {key} by {delta}",
343 "memcached_decr" => "-> memcached decr {key} by {delta}",
344 "memcached_touch" => "-> memcached touch {key} exptime={exptime}",
345 "memcached_stats" => "-> memcached stats {group}",
346 "memcached_version" => "-> memcached version",
347 "memcached_flush_all" => "-> memcached flush_all confirm={confirm} delay={delay}",
348 _ => "-> memcached disconnect",
349 };
350 ActionDefinition {
351 name: name.to_string(),
352 description: description.to_string(),
353 parameters,
354 example,
355 log_template: Some(LogTemplate::new().with_info(info)),
356 }
357}
358
359fn all_actions() -> Vec<ActionDefinition> {
360 let keys_param = param(
361 "keys",
362 "array",
363 "The keys to read (1 to 32). Each hit arrives as its own memcached_value event, each \
364 miss as memcached_miss",
365 true,
366 );
367 vec![
368 simple_action(
369 "memcached_get",
370 "Read one or more keys",
371 vec![keys_param.clone()],
372 json!({"type": "memcached_get", "keys": ["greeting", "counter"]}),
373 ),
374 simple_action(
375 "memcached_gets",
376 "Read keys together with their CAS unique, for a later memcached_cas",
377 vec![keys_param],
378 json!({"type": "memcached_gets", "keys": ["counter"]}),
379 ),
380 store_action("set", "Store a value unconditionally", false),
381 store_action("add", "Store a value only if the key does not exist", false),
382 store_action(
383 "replace",
384 "Store a value only if the key already exists",
385 false,
386 ),
387 store_action("append", "Append text to an existing value", false),
388 store_action("prepend", "Prepend text to an existing value", false),
389 store_action(
390 "cas",
391 "Store a value only if the item has not changed since memcached_gets read it",
392 true,
393 ),
394 simple_action(
395 "memcached_delete",
396 "Delete a key and its value from the server",
397 vec![key_param("The key to delete")],
398 json!({"type": "memcached_delete", "key": "greeting"}),
399 ),
400 simple_action(
401 "memcached_incr",
402 "Increment a numeric value",
403 vec![
404 key_param("The counter's key"),
405 param("delta", "number", "Amount to add (unsigned 64-bit)", true),
406 ],
407 json!({"type": "memcached_incr", "key": "hits", "delta": 1}),
408 ),
409 simple_action(
410 "memcached_decr",
411 "Decrement a numeric value (memcached stops at 0)",
412 vec![
413 key_param("The counter's key"),
414 param(
415 "delta",
416 "number",
417 "Amount to subtract (unsigned 64-bit)",
418 true,
419 ),
420 ],
421 json!({"type": "memcached_decr", "key": "hits", "delta": 1}),
422 ),
423 simple_action(
424 "memcached_touch",
425 "Change a key's expiration time without reading it",
426 vec![
427 key_param("The key whose expiration time changes"),
428 param(
429 "exptime",
430 "number",
431 "New expiration, as for memcached_set",
432 true,
433 ),
434 ],
435 json!({"type": "memcached_touch", "key": "session", "exptime": 300}),
436 ),
437 simple_action(
438 "memcached_stats",
439 "Read the server's statistics",
440 vec![param(
441 "group",
442 "string",
443 "Optional group: settings, items, slabs, sizes or conns",
444 false,
445 )],
446 json!({"type": "memcached_stats"}),
447 ),
448 simple_action(
449 "memcached_version",
450 "Ask the server for its version",
451 vec![],
452 json!({"type": "memcached_version"}),
453 ),
454 simple_action(
455 "memcached_flush_all",
456 "Invalidate EVERY item on the server. Refused unless confirm is true",
457 vec![
458 param(
459 "confirm",
460 "boolean",
461 "Must be true: this deletes everything the server holds, for every client",
462 true,
463 ),
464 param(
465 "delay",
466 "number",
467 "Seconds to wait before flushing (default: immediately)",
468 false,
469 ),
470 ],
471 json!({"type": "memcached_flush_all", "confirm": true}),
472 ),
473 simple_action(
474 "disconnect",
475 "Close the connection to the memcached server",
476 vec![],
477 json!({"type": "disconnect"}),
478 ),
479 ]
480}
481
482fn str_field<'a>(action: &'a Value, name: &str) -> Result<&'a str> {
483 action
484 .get(name)
485 .and_then(Value::as_str)
486 .with_context(|| format!("missing string field '{name}'"))
487}
488
489fn checked_key(action: &Value) -> Result<String> {
490 let key = str_field(action, "key")?;
491 validate_key(key).map_err(|e| anyhow!(e))?;
492 Ok(key.to_string())
493}
494
495fn u64_field(action: &Value, name: &str) -> Result<Option<u64>> {
496 match action.get(name) {
497 None | Some(Value::Null) => Ok(None),
498 Some(v) => v
499 .as_u64()
500 .map(Some)
501 .with_context(|| format!("'{name}' must be a non-negative integer, got {v}")),
502 }
503}
504
505fn exptime_field(action: &Value, required: bool) -> Result<i64> {
506 match action.get("exptime") {
507 None | Some(Value::Null) if !required => Ok(0),
508 None | Some(Value::Null) => Err(anyhow!("missing number field 'exptime'")),
509 Some(v) => v
510 .as_i64()
511 .with_context(|| format!("'exptime' must be an integer, got {v}")),
512 }
513}
514
515pub fn request_from_action(action: &Value) -> Result<Option<Request>> {
520 let action_type = str_field(action, "type")?;
521 let request = match action_type {
522 "memcached_get" | "memcached_gets" => {
523 let keys: Vec<String> = match action.get("keys") {
524 Some(Value::Array(items)) => items
525 .iter()
526 .map(|k| {
527 k.as_str()
528 .map(str::to_string)
529 .with_context(|| format!("every key must be a string, got {k}"))
530 })
531 .collect::<Result<_>>()?,
532 Some(Value::String(one)) => vec![one.clone()],
533 _ => return Err(anyhow!("missing 'keys' (an array of strings)")),
534 };
535 if keys.is_empty() || keys.len() > MAX_GET_KEYS {
536 return Err(anyhow!(
537 "a get names 1 to {MAX_GET_KEYS} keys; {} given",
538 keys.len()
539 ));
540 }
541 for (i, k) in keys.iter().enumerate() {
542 validate_key(k).map_err(|e| anyhow!(e))?;
543 if keys[..i].contains(k) {
544 return Err(anyhow!("key {k:?} is named twice"));
545 }
546 }
547 Request::Get {
548 keys,
549 with_cas: action_type == "memcached_gets",
550 }
551 }
552 "memcached_set" | "memcached_add" | "memcached_replace" | "memcached_append"
553 | "memcached_prepend" | "memcached_cas" => {
554 let verb = match action_type {
555 "memcached_set" => StoreVerb::Set,
556 "memcached_add" => StoreVerb::Add,
557 "memcached_replace" => StoreVerb::Replace,
558 "memcached_append" => StoreVerb::Append,
559 "memcached_prepend" => StoreVerb::Prepend,
560 _ => StoreVerb::Cas,
561 };
562 let key = checked_key(action)?;
563 let value = str_field(action, "value")?.to_string();
564 if value.len() > MAX_VALUE_LEN {
565 return Err(anyhow!(
566 "value is {} bytes; memcached's default item limit is {MAX_VALUE_LEN}",
567 value.len()
568 ));
569 }
570 let flags = match u64_field(action, "flags")? {
571 None => 0,
572 Some(f) => u32::try_from(f)
573 .map_err(|_| anyhow!("flags {f} does not fit memcached's 32-bit field"))?,
574 };
575 let exptime = exptime_field(action, false)?;
576 let cas_unique = if verb == StoreVerb::Cas {
577 Some(
578 u64_field(action, "cas_unique")?
579 .context("memcached_cas needs 'cas_unique' from a memcached_gets")?,
580 )
581 } else {
582 None
583 };
584 Request::Store {
585 verb,
586 key,
587 flags,
588 exptime,
589 value,
590 cas_unique,
591 }
592 }
593 "memcached_delete" => Request::Delete {
594 key: checked_key(action)?,
595 },
596 "memcached_incr" | "memcached_decr" => {
597 let key = checked_key(action)?;
598 let delta = u64_field(action, "delta")?.context("missing number field 'delta'")?;
599 if action_type == "memcached_incr" {
600 Request::Incr { key, delta }
601 } else {
602 Request::Decr { key, delta }
603 }
604 }
605 "memcached_touch" => Request::Touch {
606 key: checked_key(action)?,
607 exptime: exptime_field(action, true)?,
608 },
609 "memcached_stats" => {
610 let group = match action.get("group") {
611 None | Some(Value::Null) => None,
612 Some(Value::String(g)) if g.is_empty() => None,
613 Some(Value::String(g)) => {
614 validate_stats_group(g).map_err(|e| anyhow!(e))?;
615 Some(g.clone())
616 }
617 Some(other) => return Err(anyhow!("'group' must be a string, got {other}")),
618 };
619 Request::Stats { group }
620 }
621 "memcached_version" => Request::Version,
622 "memcached_flush_all" => {
623 if action.get("confirm").and_then(Value::as_bool) != Some(true) {
624 return Err(anyhow!(
625 "memcached_flush_all invalidates every item on the server for every \
626 client; it is only sent with \"confirm\": true"
627 ));
628 }
629 Request::FlushAll {
630 delay: u64_field(action, "delay")?,
631 }
632 }
633 "disconnect" => return Ok(None),
634 other => return Err(anyhow!("Unknown Memcached client action: {other}")),
635 };
636 Ok(Some(request))
637}
638
639impl Protocol for MemcachedClientProtocol {
640 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
641 all_actions()
642 }
643
644 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
645 all_actions()
646 }
647
648 fn protocol_name(&self) -> &'static str {
649 "Memcached"
650 }
651
652 fn get_event_types(&self) -> Vec<EventType> {
653 vec![
654 MEMCACHED_CONNECTED_EVENT.clone(),
655 MEMCACHED_VALUE_EVENT.clone(),
656 MEMCACHED_MISS_EVENT.clone(),
657 MEMCACHED_STORED_EVENT.clone(),
658 MEMCACHED_NOT_STORED_EVENT.clone(),
659 MEMCACHED_EXISTS_EVENT.clone(),
660 MEMCACHED_NOT_FOUND_EVENT.clone(),
661 MEMCACHED_DELETED_EVENT.clone(),
662 MEMCACHED_TOUCHED_EVENT.clone(),
663 MEMCACHED_COUNTER_EVENT.clone(),
664 MEMCACHED_STATS_EVENT.clone(),
665 MEMCACHED_VERSION_EVENT.clone(),
666 MEMCACHED_OK_EVENT.clone(),
667 MEMCACHED_ERROR_EVENT.clone(),
668 ]
669 }
670
671 fn stack_name(&self) -> &'static str {
672 "ETH>IP>TCP>Memcached"
673 }
674
675 fn keywords(&self) -> Vec<&'static str> {
676 vec![
677 "memcached",
678 "memcached client",
679 "memcache",
680 "connect to memcached",
681 ]
682 }
683
684 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
685 use crate::protocol::metadata::{
686 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
687 };
688
689 ProtocolMetadataV2::builder()
690 .state(DevelopmentState::Beta)
691 .privilege_requirement(PrivilegeRequirement::None)
692 .implementation(
693 "Hand-written memcached text-protocol client on tokio \
694 (src/client/memcached/wire.rs). One transport task owns the socket and the \
695 FIFO pairing each reply with its request; the model is asked from a separate \
696 turn task, so a parked turn never stalls the socket. Requests are never sent \
697 noreply. Reads are bounded: a 2 KiB reply line, the declared VALUE size \
698 checked against memcached's 1 MiB item limit before the block is read, only \
699 requested keys, 4096 STAT lines.",
700 )
701 .llm_control(
702 "Which keys to read and write and what to store: get/gets (up to 32 keys), \
703 set/add/replace/append/prepend/cas with flags and exptime, delete, incr/decr, \
704 touch, stats, version, and flush_all only with confirm: true.",
705 )
706 .e2e_testing(
707 "tests/client/memcached/real_server_test.rs, 8 LLM calls, against the real C \
708 memcached prepared and read back with libmemcached's memcp and memcat (a \
709 separate C client library). The model sets a value with flags 42 that memcat \
710 reads back with its flags; gets four keys and is shown two values (with CAS \
711 uniques), a non-UTF-8 value refused as non_text_value, and a miss, each as its \
712 own event matched on parsed fields; and stores a value built from the one \
713 memcp wrote, which memcat reads back. A second test drives the command channel \
714 against the same server: an injected set read back by memcat, flush_all \
715 without confirm refused before the wire, and a disconnect. Not #[ignore]d; a \
716 missing memcached, memcp or memcat fails the test rather than skipping it. \
717 wire_test.rs and in_flight_test.rs pin the framing and each bound.",
718 )
719 .notes(
720 "Text protocol only (the binary protocol is deprecated upstream). Values are \
721 text: a non-UTF-8 value is reported as memcached_error kind non_text_value, \
722 never re-encoded. No SASL, no meta commands (mg/ms), no response timeout.",
723 )
724 .build()
725 }
726
727 fn description(&self) -> &'static str {
728 "Memcached client for reading and writing cache items over the text protocol"
729 }
730
731 fn example_prompt(&self) -> &'static str {
732 "Connect to memcached at localhost:11211, store 'hello' under key greeting and read it back"
733 }
734
735 fn group_name(&self) -> &'static str {
736 "Database"
737 }
738
739 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
740 use crate::llm::actions::StartupExamples;
741
742 StartupExamples::new(
743 json!({
744 "type": "open_client",
745 "remote_addr": "localhost:11211",
746 "base_stack": "memcached",
747 "instruction": "Store 'hello' under greeting, read it back and report it"
748 }),
749 json!({
750 "type": "open_client",
751 "remote_addr": "localhost:11211",
752 "base_stack": "memcached",
753 "event_handlers": [{
754 "event_pattern": "memcached_value",
755 "handler": {
756 "type": "script",
757 "language": "python",
758 "code": "<memcached_client_handler>"
759 }
760 }]
761 }),
762 json!({
763 "type": "open_client",
764 "remote_addr": "localhost:11211",
765 "base_stack": "memcached",
766 "event_handlers": [
767 {
768 "event_pattern": "memcached_connected",
769 "handler": {
770 "type": "static",
771 "actions": [{"type": "memcached_get", "keys": ["status"]}]
772 }
773 },
774 {
775 "event_pattern": "memcached_value",
776 "handler": {"type": "static", "actions": [{"type": "disconnect"}]}
777 }
778 ]
779 }),
780 )
781 }
782}
783
784impl Client for MemcachedClientProtocol {
785 fn connect(
786 &self,
787 ctx: crate::protocol::ConnectContext,
788 ) -> std::pin::Pin<
789 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
790 > {
791 Box::pin(async move {
792 crate::client::memcached::MemcachedClient::connect_with_llm_actions(
793 ctx.remote_addr,
794 ctx.llm_client,
795 ctx.state,
796 ctx.status_tx,
797 ctx.client_id,
798 )
799 .await
800 })
801 }
802
803 fn execute_action(&self, action: Value) -> Result<ClientActionResult> {
804 match request_from_action(&action)? {
805 None => Ok(ClientActionResult::Disconnect),
806 Some(_) => Ok(ClientActionResult::Custom {
807 name: REQUEST_RESULT.to_string(),
808 data: action,
809 }),
810 }
811 }
812}