Skip to main content

netget/client/memcached/
actions.rs

1//! Memcached client actions, events and metadata.
2//!
3//! NetGet dials a memcached server and speaks the text protocol. The model decides which
4//! requests to send (`memcached_get`, `memcached_set`, …) and is shown every reply as a
5//! structured event: one `memcached_value` or `memcached_miss` per requested key, one status
6//! event per storage/delete/counter/touch reply, `memcached_stats` as a JSON object.
7//!
8//! Values travel as **text**. A value the model stores is its UTF-8 bytes; a value the server
9//! returns that is not valid UTF-8 is refused with `memcached_error {kind: "non_text_value"}`
10//! naming the key and its length, rather than re-encoded into something the model would then
11//! write back as different bytes.
12
13use 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
29/// The name of the `ClientActionResult::Custom` every wire request travels as.
30pub 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
263/// Memcached client protocol.
264pub 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
515/// Parse and validate one model action into a wire request. `Ok(None)` is `disconnect`.
516///
517/// Every refusal happens here, before anything reaches the wire, so the model gets a reason
518/// rather than a CLIENT_ERROR it has to interpret.
519pub 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}