Skip to main content

pylon_plugin/builtin/
cache.rs

1use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
2use std::sync::Mutex;
3use std::time::{Duration, Instant};
4
5use crate::Plugin;
6use serde::Serialize;
7
8// ---------------------------------------------------------------------------
9// Data model
10// ---------------------------------------------------------------------------
11
12/// The value types the cache can store.
13#[derive(Debug, Clone)]
14#[allow(dead_code)]
15enum CacheValue {
16    String(String),
17    Int(i64),
18    Float(f64),
19    List(VecDeque<String>),
20    Set(HashSet<String>),
21    Hash(HashMap<String, String>),
22    SortedSet(BTreeMap<String, f64>),
23}
24
25/// An entry in the cache with optional expiration.
26struct CacheEntry {
27    value: CacheValue,
28    expires_at: Option<Instant>,
29    #[allow(dead_code)]
30    created_at: Instant,
31    last_accessed: Instant,
32}
33
34impl CacheEntry {
35    fn new(value: CacheValue, ttl: Option<u64>) -> Self {
36        let now = Instant::now();
37        Self {
38            value,
39            expires_at: ttl.map(|s| now + Duration::from_secs(s)),
40            created_at: now,
41            last_accessed: now,
42        }
43    }
44
45    fn is_expired(&self) -> bool {
46        self.expires_at
47            .map(|exp| Instant::now() >= exp)
48            .unwrap_or(false)
49    }
50
51    fn touch(&mut self) {
52        self.last_accessed = Instant::now();
53    }
54}
55
56/// The cache engine -- a Redis-like in-memory data structure store.
57pub struct CachePlugin {
58    store: Mutex<HashMap<String, CacheEntry>>,
59    max_keys: usize,
60    stats: Mutex<CacheStats>,
61}
62
63#[derive(Debug, Clone, Default, Serialize)]
64pub struct CacheStats {
65    pub hits: u64,
66    pub misses: u64,
67    pub sets: u64,
68    pub deletes: u64,
69    pub evictions: u64,
70    pub expired: u64,
71}
72
73// ---------------------------------------------------------------------------
74// Glob matching
75// ---------------------------------------------------------------------------
76
77fn glob_match(pattern: &str, text: &str) -> bool {
78    let mut pi = 0;
79    let mut ti = 0;
80    let pb = pattern.as_bytes();
81    let tb = text.as_bytes();
82    let mut star_pi = usize::MAX;
83    let mut star_ti = 0;
84
85    while ti < tb.len() {
86        if pi < pb.len() && (pb[pi] == b'?' || pb[pi] == tb[ti]) {
87            pi += 1;
88            ti += 1;
89        } else if pi < pb.len() && pb[pi] == b'*' {
90            star_pi = pi;
91            star_ti = ti;
92            pi += 1;
93        } else if star_pi != usize::MAX {
94            pi = star_pi + 1;
95            star_ti += 1;
96            ti = star_ti;
97        } else {
98            return false;
99        }
100    }
101    while pi < pb.len() && pb[pi] == b'*' {
102        pi += 1;
103    }
104    pi == pb.len()
105}
106
107// ---------------------------------------------------------------------------
108// Implementation
109// ---------------------------------------------------------------------------
110
111impl CachePlugin {
112    pub fn new(max_keys: usize) -> Self {
113        Self {
114            store: Mutex::new(HashMap::new()),
115            max_keys,
116            stats: Mutex::new(CacheStats::default()),
117        }
118    }
119
120    // -- internal helpers ---------------------------------------------------
121
122    /// Record a cache hit.
123    fn record_hit(&self) {
124        self.stats.lock().unwrap().hits += 1;
125    }
126
127    /// Record a cache miss.
128    fn record_miss(&self) {
129        self.stats.lock().unwrap().misses += 1;
130    }
131
132    /// Evict the least-recently-used key to make room for a new entry.
133    /// Caller must already hold the store lock.
134    fn evict_lru(&self, store: &mut HashMap<String, CacheEntry>) {
135        if store.len() < self.max_keys {
136            return;
137        }
138
139        // Find the key with the oldest last_accessed timestamp.
140        let victim = store
141            .iter()
142            .min_by_key(|(_, entry)| entry.last_accessed)
143            .map(|(k, _)| k.clone());
144
145        if let Some(key) = victim {
146            store.remove(&key);
147            self.stats.lock().unwrap().evictions += 1;
148        }
149    }
150
151    /// Remove a key if it is expired. Returns true if the key was removed.
152    /// Caller must already hold the store lock.
153    fn remove_if_expired(&self, store: &mut HashMap<String, CacheEntry>, key: &str) -> bool {
154        let expired = store.get(key).map(|e| e.is_expired()).unwrap_or(false);
155        if expired {
156            store.remove(key);
157            self.stats.lock().unwrap().expired += 1;
158        }
159        expired
160    }
161
162    // -----------------------------------------------------------------------
163    // String operations
164    // -----------------------------------------------------------------------
165
166    /// SET key value [EX seconds]
167    pub fn set(&self, key: &str, value: &str, ttl: Option<u64>) {
168        let mut store = self.store.lock().unwrap();
169        self.evict_lru(&mut store);
170        store.insert(
171            key.to_string(),
172            CacheEntry::new(CacheValue::String(value.to_string()), ttl),
173        );
174        self.stats.lock().unwrap().sets += 1;
175    }
176
177    /// GET key -- returns None if expired or missing.
178    pub fn get(&self, key: &str) -> Option<String> {
179        let mut store = self.store.lock().unwrap();
180        if self.remove_if_expired(&mut store, key) {
181            self.record_miss();
182            return None;
183        }
184        match store.get_mut(key) {
185            Some(entry) => {
186                entry.touch();
187                let val = match &entry.value {
188                    CacheValue::String(s) => Some(s.clone()),
189                    CacheValue::Int(n) => Some(n.to_string()),
190                    CacheValue::Float(f) => Some(f.to_string()),
191                    _ => None,
192                };
193                if val.is_some() {
194                    self.record_hit();
195                } else {
196                    self.record_miss();
197                }
198                val
199            }
200            None => {
201                self.record_miss();
202                None
203            }
204        }
205    }
206
207    /// DEL key -- returns true if key existed.
208    pub fn del(&self, key: &str) -> bool {
209        let mut store = self.store.lock().unwrap();
210        let existed = store.remove(key).is_some();
211        if existed {
212            self.stats.lock().unwrap().deletes += 1;
213        }
214        existed
215    }
216
217    /// EXISTS key
218    pub fn exists(&self, key: &str) -> bool {
219        let mut store = self.store.lock().unwrap();
220        if self.remove_if_expired(&mut store, key) {
221            return false;
222        }
223        store.contains_key(key)
224    }
225
226    /// INCR key -- increment integer value, creates if not exists (starts at 0).
227    pub fn incr(&self, key: &str) -> Result<i64, String> {
228        self.incrby(key, 1)
229    }
230
231    /// DECR key
232    pub fn decr(&self, key: &str) -> Result<i64, String> {
233        self.incrby(key, -1)
234    }
235
236    /// INCRBY key amount
237    pub fn incrby(&self, key: &str, amount: i64) -> Result<i64, String> {
238        let mut store = self.store.lock().unwrap();
239        self.remove_if_expired(&mut store, key);
240
241        match store.get_mut(key) {
242            Some(entry) => {
243                entry.touch();
244                match &mut entry.value {
245                    CacheValue::Int(n) => {
246                        *n += amount;
247                        Ok(*n)
248                    }
249                    CacheValue::String(s) => {
250                        let n: i64 = s
251                            .parse()
252                            .map_err(|_| "value is not an integer".to_string())?;
253                        let new_val = n + amount;
254                        entry.value = CacheValue::Int(new_val);
255                        Ok(new_val)
256                    }
257                    _ => Err("value is not an integer".to_string()),
258                }
259            }
260            None => {
261                self.evict_lru(&mut store);
262                store.insert(
263                    key.to_string(),
264                    CacheEntry::new(CacheValue::Int(amount), None),
265                );
266                Ok(amount)
267            }
268        }
269    }
270
271    /// SETNX -- set only if key doesn't exist. Returns true if set.
272    pub fn setnx(&self, key: &str, value: &str, ttl: Option<u64>) -> bool {
273        let mut store = self.store.lock().unwrap();
274        self.remove_if_expired(&mut store, key);
275
276        if store.contains_key(key) {
277            return false;
278        }
279
280        self.evict_lru(&mut store);
281        store.insert(
282            key.to_string(),
283            CacheEntry::new(CacheValue::String(value.to_string()), ttl),
284        );
285        self.stats.lock().unwrap().sets += 1;
286        true
287    }
288
289    /// GETSET -- set new value and return old value.
290    pub fn getset(&self, key: &str, value: &str) -> Option<String> {
291        let mut store = self.store.lock().unwrap();
292        self.remove_if_expired(&mut store, key);
293
294        let old = store.get(key).and_then(|entry| match &entry.value {
295            CacheValue::String(s) => Some(s.clone()),
296            CacheValue::Int(n) => Some(n.to_string()),
297            CacheValue::Float(f) => Some(f.to_string()),
298            _ => None,
299        });
300
301        self.evict_lru(&mut store);
302        store.insert(
303            key.to_string(),
304            CacheEntry::new(CacheValue::String(value.to_string()), None),
305        );
306        self.stats.lock().unwrap().sets += 1;
307        old
308    }
309
310    /// MGET -- get multiple keys.
311    pub fn mget(&self, keys: &[&str]) -> Vec<Option<String>> {
312        let mut store = self.store.lock().unwrap();
313        keys.iter()
314            .map(|key| {
315                if self.remove_if_expired(&mut store, key) {
316                    self.record_miss();
317                    return None;
318                }
319                match store.get_mut(*key) {
320                    Some(entry) => {
321                        entry.touch();
322                        match &entry.value {
323                            CacheValue::String(s) => {
324                                self.record_hit();
325                                Some(s.clone())
326                            }
327                            CacheValue::Int(n) => {
328                                self.record_hit();
329                                Some(n.to_string())
330                            }
331                            CacheValue::Float(f) => {
332                                self.record_hit();
333                                Some(f.to_string())
334                            }
335                            _ => {
336                                self.record_miss();
337                                None
338                            }
339                        }
340                    }
341                    None => {
342                        self.record_miss();
343                        None
344                    }
345                }
346            })
347            .collect()
348    }
349
350    /// MSET -- set multiple keys.
351    pub fn mset(&self, pairs: &[(&str, &str)]) {
352        let mut store = self.store.lock().unwrap();
353        for (key, value) in pairs {
354            self.evict_lru(&mut store);
355            store.insert(
356                key.to_string(),
357                CacheEntry::new(CacheValue::String(value.to_string()), None),
358            );
359            self.stats.lock().unwrap().sets += 1;
360        }
361    }
362
363    /// TTL key -- returns remaining seconds, -1 if no expiry, -2 if key doesn't exist.
364    pub fn ttl(&self, key: &str) -> i64 {
365        let mut store = self.store.lock().unwrap();
366        if self.remove_if_expired(&mut store, key) {
367            return -2;
368        }
369        match store.get(key) {
370            Some(entry) => match entry.expires_at {
371                Some(exp) => {
372                    let now = Instant::now();
373                    if exp > now {
374                        (exp - now).as_secs() as i64
375                    } else {
376                        -2
377                    }
378                }
379                None => -1,
380            },
381            None => -2,
382        }
383    }
384
385    /// EXPIRE key seconds -- set expiry on existing key.
386    pub fn expire(&self, key: &str, seconds: u64) -> bool {
387        let mut store = self.store.lock().unwrap();
388        if self.remove_if_expired(&mut store, key) {
389            return false;
390        }
391        match store.get_mut(key) {
392            Some(entry) => {
393                entry.expires_at = Some(Instant::now() + Duration::from_secs(seconds));
394                true
395            }
396            None => false,
397        }
398    }
399
400    /// PERSIST key -- remove expiry.
401    pub fn persist(&self, key: &str) -> bool {
402        let mut store = self.store.lock().unwrap();
403        if self.remove_if_expired(&mut store, key) {
404            return false;
405        }
406        match store.get_mut(key) {
407            Some(entry) => {
408                let had_expiry = entry.expires_at.is_some();
409                entry.expires_at = None;
410                had_expiry
411            }
412            None => false,
413        }
414    }
415
416    /// KEYS pattern -- find keys matching glob pattern.
417    pub fn keys(&self, pattern: &str) -> Vec<String> {
418        let mut store = self.store.lock().unwrap();
419
420        // First collect expired keys so we can remove them.
421        let expired: Vec<String> = store
422            .iter()
423            .filter(|(_, entry)| entry.is_expired())
424            .map(|(k, _)| k.clone())
425            .collect();
426        for k in &expired {
427            store.remove(k);
428        }
429        {
430            let mut stats = self.stats.lock().unwrap();
431            stats.expired += expired.len() as u64;
432        }
433
434        store
435            .keys()
436            .filter(|k| glob_match(pattern, k))
437            .cloned()
438            .collect()
439    }
440
441    // -----------------------------------------------------------------------
442    // List operations
443    // -----------------------------------------------------------------------
444
445    /// LPUSH key value -- push to front, creates list if needed.
446    pub fn lpush(&self, key: &str, value: &str) -> usize {
447        let mut store = self.store.lock().unwrap();
448        self.remove_if_expired(&mut store, key);
449
450        match store.get_mut(key) {
451            Some(entry) => {
452                entry.touch();
453                if let CacheValue::List(list) = &mut entry.value {
454                    list.push_front(value.to_string());
455                    list.len()
456                } else {
457                    // Replace with a new list containing the value.
458                    let mut list = VecDeque::new();
459                    list.push_front(value.to_string());
460                    let len = list.len();
461                    entry.value = CacheValue::List(list);
462                    len
463                }
464            }
465            None => {
466                self.evict_lru(&mut store);
467                let mut list = VecDeque::new();
468                list.push_front(value.to_string());
469                store.insert(
470                    key.to_string(),
471                    CacheEntry::new(CacheValue::List(list), None),
472                );
473                1
474            }
475        }
476    }
477
478    /// RPUSH key value -- push to back.
479    pub fn rpush(&self, key: &str, value: &str) -> usize {
480        let mut store = self.store.lock().unwrap();
481        self.remove_if_expired(&mut store, key);
482
483        match store.get_mut(key) {
484            Some(entry) => {
485                entry.touch();
486                if let CacheValue::List(list) = &mut entry.value {
487                    list.push_back(value.to_string());
488                    list.len()
489                } else {
490                    let mut list = VecDeque::new();
491                    list.push_back(value.to_string());
492                    let len = list.len();
493                    entry.value = CacheValue::List(list);
494                    len
495                }
496            }
497            None => {
498                self.evict_lru(&mut store);
499                let mut list = VecDeque::new();
500                list.push_back(value.to_string());
501                store.insert(
502                    key.to_string(),
503                    CacheEntry::new(CacheValue::List(list), None),
504                );
505                1
506            }
507        }
508    }
509
510    /// LPOP key -- pop from front.
511    pub fn lpop(&self, key: &str) -> Option<String> {
512        let mut store = self.store.lock().unwrap();
513        if self.remove_if_expired(&mut store, key) {
514            return None;
515        }
516        let entry = store.get_mut(key)?;
517        entry.touch();
518        if let CacheValue::List(list) = &mut entry.value {
519            list.pop_front()
520        } else {
521            None
522        }
523    }
524
525    /// RPOP key -- pop from back.
526    pub fn rpop(&self, key: &str) -> Option<String> {
527        let mut store = self.store.lock().unwrap();
528        if self.remove_if_expired(&mut store, key) {
529            return None;
530        }
531        let entry = store.get_mut(key)?;
532        entry.touch();
533        if let CacheValue::List(list) = &mut entry.value {
534            list.pop_back()
535        } else {
536            None
537        }
538    }
539
540    /// LRANGE key start stop -- get range (inclusive, supports negative indices).
541    pub fn lrange(&self, key: &str, start: i64, stop: i64) -> Vec<String> {
542        let mut store = self.store.lock().unwrap();
543        if self.remove_if_expired(&mut store, key) {
544            return vec![];
545        }
546        match store.get_mut(key) {
547            Some(entry) => {
548                entry.touch();
549                if let CacheValue::List(list) = &entry.value {
550                    let len = list.len() as i64;
551                    if len == 0 {
552                        return vec![];
553                    }
554
555                    // Resolve negative indices.
556                    let s = if start < 0 {
557                        (len + start).max(0) as usize
558                    } else {
559                        start.min(len - 1) as usize
560                    };
561                    let e = if stop < 0 {
562                        (len + stop).max(0) as usize
563                    } else {
564                        stop.min(len - 1) as usize
565                    };
566
567                    if s > e {
568                        return vec![];
569                    }
570
571                    list.iter().skip(s).take(e - s + 1).cloned().collect()
572                } else {
573                    vec![]
574                }
575            }
576            None => vec![],
577        }
578    }
579
580    /// LLEN key -- list length.
581    pub fn llen(&self, key: &str) -> usize {
582        let mut store = self.store.lock().unwrap();
583        if self.remove_if_expired(&mut store, key) {
584            return 0;
585        }
586        match store.get(key) {
587            Some(entry) => {
588                if let CacheValue::List(list) = &entry.value {
589                    list.len()
590                } else {
591                    0
592                }
593            }
594            None => 0,
595        }
596    }
597
598    // -----------------------------------------------------------------------
599    // Set operations
600    // -----------------------------------------------------------------------
601
602    /// SADD key member -- add to set. Returns true if the member was newly added.
603    pub fn sadd(&self, key: &str, member: &str) -> bool {
604        let mut store = self.store.lock().unwrap();
605        self.remove_if_expired(&mut store, key);
606
607        match store.get_mut(key) {
608            Some(entry) => {
609                entry.touch();
610                if let CacheValue::Set(set) = &mut entry.value {
611                    set.insert(member.to_string())
612                } else {
613                    let mut set = HashSet::new();
614                    set.insert(member.to_string());
615                    entry.value = CacheValue::Set(set);
616                    true
617                }
618            }
619            None => {
620                self.evict_lru(&mut store);
621                let mut set = HashSet::new();
622                set.insert(member.to_string());
623                store.insert(key.to_string(), CacheEntry::new(CacheValue::Set(set), None));
624                true
625            }
626        }
627    }
628
629    /// SREM key member -- remove from set.
630    pub fn srem(&self, key: &str, member: &str) -> bool {
631        let mut store = self.store.lock().unwrap();
632        if self.remove_if_expired(&mut store, key) {
633            return false;
634        }
635        match store.get_mut(key) {
636            Some(entry) => {
637                entry.touch();
638                if let CacheValue::Set(set) = &mut entry.value {
639                    set.remove(member)
640                } else {
641                    false
642                }
643            }
644            None => false,
645        }
646    }
647
648    /// SMEMBERS key -- all members.
649    pub fn smembers(&self, key: &str) -> Vec<String> {
650        let mut store = self.store.lock().unwrap();
651        if self.remove_if_expired(&mut store, key) {
652            return vec![];
653        }
654        match store.get_mut(key) {
655            Some(entry) => {
656                entry.touch();
657                if let CacheValue::Set(set) = &entry.value {
658                    set.iter().cloned().collect()
659                } else {
660                    vec![]
661                }
662            }
663            None => vec![],
664        }
665    }
666
667    /// SISMEMBER key member
668    pub fn sismember(&self, key: &str, member: &str) -> bool {
669        let mut store = self.store.lock().unwrap();
670        if self.remove_if_expired(&mut store, key) {
671            return false;
672        }
673        match store.get(key) {
674            Some(entry) => {
675                if let CacheValue::Set(set) = &entry.value {
676                    set.contains(member)
677                } else {
678                    false
679                }
680            }
681            None => false,
682        }
683    }
684
685    /// SCARD key -- set size.
686    pub fn scard(&self, key: &str) -> usize {
687        let mut store = self.store.lock().unwrap();
688        if self.remove_if_expired(&mut store, key) {
689            return 0;
690        }
691        match store.get(key) {
692            Some(entry) => {
693                if let CacheValue::Set(set) = &entry.value {
694                    set.len()
695                } else {
696                    0
697                }
698            }
699            None => 0,
700        }
701    }
702
703    /// SINTER key1 key2 -- intersection of two sets.
704    pub fn sinter(&self, key1: &str, key2: &str) -> Vec<String> {
705        let mut store = self.store.lock().unwrap();
706        self.remove_if_expired(&mut store, key1);
707        self.remove_if_expired(&mut store, key2);
708
709        let set1 = match store.get(key1) {
710            Some(entry) => match &entry.value {
711                CacheValue::Set(s) => s.clone(),
712                _ => return vec![],
713            },
714            None => return vec![],
715        };
716        let set2 = match store.get(key2) {
717            Some(entry) => match &entry.value {
718                CacheValue::Set(s) => s,
719                _ => return vec![],
720            },
721            None => return vec![],
722        };
723
724        set1.intersection(set2).cloned().collect()
725    }
726
727    /// SUNION key1 key2 -- union of two sets.
728    pub fn sunion(&self, key1: &str, key2: &str) -> Vec<String> {
729        let mut store = self.store.lock().unwrap();
730        self.remove_if_expired(&mut store, key1);
731        self.remove_if_expired(&mut store, key2);
732
733        let set1 = match store.get(key1) {
734            Some(entry) => match &entry.value {
735                CacheValue::Set(s) => s.clone(),
736                _ => HashSet::new(),
737            },
738            None => HashSet::new(),
739        };
740        let set2 = match store.get(key2) {
741            Some(entry) => match &entry.value {
742                CacheValue::Set(s) => s,
743                _ => return set1.into_iter().collect(),
744            },
745            None => return set1.into_iter().collect(),
746        };
747
748        set1.union(set2).cloned().collect()
749    }
750
751    // -----------------------------------------------------------------------
752    // Hash operations
753    // -----------------------------------------------------------------------
754
755    /// HSET key field value
756    pub fn hset(&self, key: &str, field: &str, value: &str) {
757        let mut store = self.store.lock().unwrap();
758        self.remove_if_expired(&mut store, key);
759
760        match store.get_mut(key) {
761            Some(entry) => {
762                entry.touch();
763                if let CacheValue::Hash(hash) = &mut entry.value {
764                    hash.insert(field.to_string(), value.to_string());
765                } else {
766                    let mut hash = HashMap::new();
767                    hash.insert(field.to_string(), value.to_string());
768                    entry.value = CacheValue::Hash(hash);
769                }
770            }
771            None => {
772                self.evict_lru(&mut store);
773                let mut hash = HashMap::new();
774                hash.insert(field.to_string(), value.to_string());
775                store.insert(
776                    key.to_string(),
777                    CacheEntry::new(CacheValue::Hash(hash), None),
778                );
779            }
780        }
781    }
782
783    /// HGET key field
784    pub fn hget(&self, key: &str, field: &str) -> Option<String> {
785        let mut store = self.store.lock().unwrap();
786        if self.remove_if_expired(&mut store, key) {
787            return None;
788        }
789        let entry = store.get_mut(key)?;
790        entry.touch();
791        if let CacheValue::Hash(hash) = &entry.value {
792            hash.get(field).cloned()
793        } else {
794            None
795        }
796    }
797
798    /// HDEL key field
799    pub fn hdel(&self, key: &str, field: &str) -> bool {
800        let mut store = self.store.lock().unwrap();
801        if self.remove_if_expired(&mut store, key) {
802            return false;
803        }
804        match store.get_mut(key) {
805            Some(entry) => {
806                entry.touch();
807                if let CacheValue::Hash(hash) = &mut entry.value {
808                    hash.remove(field).is_some()
809                } else {
810                    false
811                }
812            }
813            None => false,
814        }
815    }
816
817    /// HGETALL key -- all field-value pairs.
818    pub fn hgetall(&self, key: &str) -> HashMap<String, String> {
819        let mut store = self.store.lock().unwrap();
820        if self.remove_if_expired(&mut store, key) {
821            return HashMap::new();
822        }
823        match store.get_mut(key) {
824            Some(entry) => {
825                entry.touch();
826                if let CacheValue::Hash(hash) = &entry.value {
827                    hash.clone()
828                } else {
829                    HashMap::new()
830                }
831            }
832            None => HashMap::new(),
833        }
834    }
835
836    /// HEXISTS key field
837    pub fn hexists(&self, key: &str, field: &str) -> bool {
838        let mut store = self.store.lock().unwrap();
839        if self.remove_if_expired(&mut store, key) {
840            return false;
841        }
842        match store.get(key) {
843            Some(entry) => {
844                if let CacheValue::Hash(hash) = &entry.value {
845                    hash.contains_key(field)
846                } else {
847                    false
848                }
849            }
850            None => false,
851        }
852    }
853
854    /// HLEN key
855    pub fn hlen(&self, key: &str) -> usize {
856        let mut store = self.store.lock().unwrap();
857        if self.remove_if_expired(&mut store, key) {
858            return 0;
859        }
860        match store.get(key) {
861            Some(entry) => {
862                if let CacheValue::Hash(hash) = &entry.value {
863                    hash.len()
864                } else {
865                    0
866                }
867            }
868            None => 0,
869        }
870    }
871
872    /// HKEYS key -- all field names.
873    pub fn hkeys(&self, key: &str) -> Vec<String> {
874        let mut store = self.store.lock().unwrap();
875        if self.remove_if_expired(&mut store, key) {
876            return vec![];
877        }
878        match store.get_mut(key) {
879            Some(entry) => {
880                entry.touch();
881                if let CacheValue::Hash(hash) = &entry.value {
882                    hash.keys().cloned().collect()
883                } else {
884                    vec![]
885                }
886            }
887            None => vec![],
888        }
889    }
890
891    /// HINCRBY key field amount
892    pub fn hincrby(&self, key: &str, field: &str, amount: i64) -> Result<i64, String> {
893        let mut store = self.store.lock().unwrap();
894        self.remove_if_expired(&mut store, key);
895
896        match store.get_mut(key) {
897            Some(entry) => {
898                entry.touch();
899                if let CacheValue::Hash(hash) = &mut entry.value {
900                    let current: i64 = match hash.get(field) {
901                        Some(v) => v
902                            .parse()
903                            .map_err(|_| "hash value is not an integer".to_string())?,
904                        None => 0,
905                    };
906                    let new_val = current + amount;
907                    hash.insert(field.to_string(), new_val.to_string());
908                    Ok(new_val)
909                } else {
910                    Err("key is not a hash".to_string())
911                }
912            }
913            None => {
914                self.evict_lru(&mut store);
915                let mut hash = HashMap::new();
916                hash.insert(field.to_string(), amount.to_string());
917                store.insert(
918                    key.to_string(),
919                    CacheEntry::new(CacheValue::Hash(hash), None),
920                );
921                Ok(amount)
922            }
923        }
924    }
925
926    // -----------------------------------------------------------------------
927    // Sorted set operations
928    // -----------------------------------------------------------------------
929
930    /// ZADD key score member
931    pub fn zadd(&self, key: &str, score: f64, member: &str) {
932        let mut store = self.store.lock().unwrap();
933        self.remove_if_expired(&mut store, key);
934
935        match store.get_mut(key) {
936            Some(entry) => {
937                entry.touch();
938                if let CacheValue::SortedSet(zset) = &mut entry.value {
939                    zset.insert(member.to_string(), score);
940                } else {
941                    let mut zset = BTreeMap::new();
942                    zset.insert(member.to_string(), score);
943                    entry.value = CacheValue::SortedSet(zset);
944                }
945            }
946            None => {
947                self.evict_lru(&mut store);
948                let mut zset = BTreeMap::new();
949                zset.insert(member.to_string(), score);
950                store.insert(
951                    key.to_string(),
952                    CacheEntry::new(CacheValue::SortedSet(zset), None),
953                );
954            }
955        }
956    }
957
958    /// ZREM key member
959    pub fn zrem(&self, key: &str, member: &str) -> bool {
960        let mut store = self.store.lock().unwrap();
961        if self.remove_if_expired(&mut store, key) {
962            return false;
963        }
964        match store.get_mut(key) {
965            Some(entry) => {
966                entry.touch();
967                if let CacheValue::SortedSet(zset) = &mut entry.value {
968                    zset.remove(member).is_some()
969                } else {
970                    false
971                }
972            }
973            None => false,
974        }
975    }
976
977    /// ZSCORE key member
978    pub fn zscore(&self, key: &str, member: &str) -> Option<f64> {
979        let mut store = self.store.lock().unwrap();
980        if self.remove_if_expired(&mut store, key) {
981            return None;
982        }
983        let entry = store.get_mut(key)?;
984        entry.touch();
985        if let CacheValue::SortedSet(zset) = &entry.value {
986            zset.get(member).copied()
987        } else {
988            None
989        }
990    }
991
992    /// ZRANK key member -- rank by score (0-based).
993    pub fn zrank(&self, key: &str, member: &str) -> Option<usize> {
994        let mut store = self.store.lock().unwrap();
995        if self.remove_if_expired(&mut store, key) {
996            return None;
997        }
998        let entry = store.get_mut(key)?;
999        entry.touch();
1000        if let CacheValue::SortedSet(zset) = &entry.value {
1001            let target_score = zset.get(member)?;
1002            // Sort by score, then by member name for deterministic ordering.
1003            let mut members: Vec<(&String, &f64)> = zset.iter().collect();
1004            members.sort_by(|a, b| {
1005                a.1.partial_cmp(b.1)
1006                    .unwrap_or(std::cmp::Ordering::Equal)
1007                    .then_with(|| a.0.cmp(b.0))
1008            });
1009            members
1010                .iter()
1011                .position(|(m, s)| *m == member && *s == target_score)
1012        } else {
1013            None
1014        }
1015    }
1016
1017    /// ZRANGE key start stop -- members by rank range (inclusive).
1018    pub fn zrange(&self, key: &str, start: usize, stop: usize) -> Vec<(String, f64)> {
1019        let mut store = self.store.lock().unwrap();
1020        if self.remove_if_expired(&mut store, key) {
1021            return vec![];
1022        }
1023        match store.get_mut(key) {
1024            Some(entry) => {
1025                entry.touch();
1026                if let CacheValue::SortedSet(zset) = &entry.value {
1027                    let mut members: Vec<(String, f64)> =
1028                        zset.iter().map(|(m, s)| (m.clone(), *s)).collect();
1029                    members.sort_by(|a, b| {
1030                        a.1.partial_cmp(&b.1)
1031                            .unwrap_or(std::cmp::Ordering::Equal)
1032                            .then_with(|| a.0.cmp(&b.0))
1033                    });
1034                    let end = stop.min(members.len().saturating_sub(1));
1035                    if start > end {
1036                        return vec![];
1037                    }
1038                    members[start..=end].to_vec()
1039                } else {
1040                    vec![]
1041                }
1042            }
1043            None => vec![],
1044        }
1045    }
1046
1047    /// ZCARD key -- sorted set size.
1048    pub fn zcard(&self, key: &str) -> usize {
1049        let mut store = self.store.lock().unwrap();
1050        if self.remove_if_expired(&mut store, key) {
1051            return 0;
1052        }
1053        match store.get(key) {
1054            Some(entry) => {
1055                if let CacheValue::SortedSet(zset) = &entry.value {
1056                    zset.len()
1057                } else {
1058                    0
1059                }
1060            }
1061            None => 0,
1062        }
1063    }
1064
1065    // -----------------------------------------------------------------------
1066    // Utility operations
1067    // -----------------------------------------------------------------------
1068
1069    /// DBSIZE -- total key count (excluding expired).
1070    pub fn dbsize(&self) -> usize {
1071        let store = self.store.lock().unwrap();
1072        store.values().filter(|e| !e.is_expired()).count()
1073    }
1074
1075    /// FLUSHALL -- delete everything.
1076    pub fn flushall(&self) {
1077        let mut store = self.store.lock().unwrap();
1078        store.clear();
1079        let mut stats = self.stats.lock().unwrap();
1080        *stats = CacheStats::default();
1081    }
1082
1083    /// INFO -- cache statistics.
1084    pub fn info(&self) -> CacheStats {
1085        self.stats.lock().unwrap().clone()
1086    }
1087
1088    /// TYPE key -- returns the type of value stored.
1089    pub fn key_type(&self, key: &str) -> Option<&'static str> {
1090        let mut store = self.store.lock().unwrap();
1091        if self.remove_if_expired(&mut store, key) {
1092            return None;
1093        }
1094        store.get(key).map(|entry| match &entry.value {
1095            CacheValue::String(_) => "string",
1096            CacheValue::Int(_) => "string",
1097            CacheValue::Float(_) => "string",
1098            CacheValue::List(_) => "list",
1099            CacheValue::Set(_) => "set",
1100            CacheValue::Hash(_) => "hash",
1101            CacheValue::SortedSet(_) => "zset",
1102        })
1103    }
1104
1105    /// Cleanup expired keys (call periodically). Returns number of keys removed.
1106    pub fn cleanup_expired(&self) -> usize {
1107        let mut store = self.store.lock().unwrap();
1108        let expired: Vec<String> = store
1109            .iter()
1110            .filter(|(_, entry)| entry.is_expired())
1111            .map(|(k, _)| k.clone())
1112            .collect();
1113        let count = expired.len();
1114        for k in &expired {
1115            store.remove(k);
1116        }
1117        self.stats.lock().unwrap().expired += count as u64;
1118        count
1119    }
1120}
1121
1122// ---------------------------------------------------------------------------
1123// Plugin trait implementation
1124// ---------------------------------------------------------------------------
1125
1126impl Plugin for CachePlugin {
1127    fn name(&self) -> &str {
1128        "cache"
1129    }
1130}
1131
1132// ---------------------------------------------------------------------------
1133// Tests
1134// ---------------------------------------------------------------------------
1135
1136#[cfg(test)]
1137mod tests {
1138    use super::*;
1139    use std::thread;
1140    use std::time::Duration;
1141
1142    fn cache() -> CachePlugin {
1143        CachePlugin::new(1000)
1144    }
1145
1146    // -- String operations --------------------------------------------------
1147
1148    #[test]
1149    fn set_and_get() {
1150        let c = cache();
1151        c.set("hello", "world", None);
1152        assert_eq!(c.get("hello"), Some("world".to_string()));
1153    }
1154
1155    #[test]
1156    fn get_missing_key_returns_none() {
1157        let c = cache();
1158        assert_eq!(c.get("nonexistent"), None);
1159    }
1160
1161    #[test]
1162    fn set_with_ttl_and_get_before_expiry() {
1163        let c = cache();
1164        c.set("k", "v", Some(10));
1165        assert_eq!(c.get("k"), Some("v".to_string()));
1166    }
1167
1168    #[test]
1169    fn get_expired_key_returns_none() {
1170        let c = cache();
1171        c.set("k", "v", Some(0));
1172        // TTL of 0 seconds means it expires immediately.
1173        thread::sleep(Duration::from_millis(5));
1174        assert_eq!(c.get("k"), None);
1175    }
1176
1177    #[test]
1178    fn incr_creates_key() {
1179        let c = cache();
1180        assert_eq!(c.incr("counter"), Ok(1));
1181        assert_eq!(c.incr("counter"), Ok(2));
1182    }
1183
1184    #[test]
1185    fn decr_key() {
1186        let c = cache();
1187        c.set("x", "10", None);
1188        assert_eq!(c.decr("x"), Ok(9));
1189        assert_eq!(c.decr("x"), Ok(8));
1190    }
1191
1192    #[test]
1193    fn incrby_amount() {
1194        let c = cache();
1195        assert_eq!(c.incrby("n", 5), Ok(5));
1196        assert_eq!(c.incrby("n", 3), Ok(8));
1197        assert_eq!(c.incrby("n", -2), Ok(6));
1198    }
1199
1200    #[test]
1201    fn incr_non_integer_errors() {
1202        let c = cache();
1203        c.set("s", "not_a_number", None);
1204        assert!(c.incr("s").is_err());
1205    }
1206
1207    #[test]
1208    fn setnx_only_sets_if_missing() {
1209        let c = cache();
1210        assert!(c.setnx("k", "first", None));
1211        assert!(!c.setnx("k", "second", None));
1212        assert_eq!(c.get("k"), Some("first".to_string()));
1213    }
1214
1215    #[test]
1216    fn getset_swaps_value() {
1217        let c = cache();
1218        c.set("k", "old", None);
1219        let old = c.getset("k", "new");
1220        assert_eq!(old, Some("old".to_string()));
1221        assert_eq!(c.get("k"), Some("new".to_string()));
1222    }
1223
1224    #[test]
1225    fn getset_on_missing_key() {
1226        let c = cache();
1227        let old = c.getset("k", "val");
1228        assert_eq!(old, None);
1229        assert_eq!(c.get("k"), Some("val".to_string()));
1230    }
1231
1232    #[test]
1233    fn mget_and_mset() {
1234        let c = cache();
1235        c.mset(&[("a", "1"), ("b", "2"), ("c", "3")]);
1236        let vals = c.mget(&["a", "b", "missing", "c"]);
1237        assert_eq!(
1238            vals,
1239            vec![
1240                Some("1".to_string()),
1241                Some("2".to_string()),
1242                None,
1243                Some("3".to_string()),
1244            ]
1245        );
1246    }
1247
1248    // -- TTL operations -----------------------------------------------------
1249
1250    #[test]
1251    fn ttl_no_expiry() {
1252        let c = cache();
1253        c.set("k", "v", None);
1254        assert_eq!(c.ttl("k"), -1);
1255    }
1256
1257    #[test]
1258    fn ttl_missing_key() {
1259        let c = cache();
1260        assert_eq!(c.ttl("nope"), -2);
1261    }
1262
1263    #[test]
1264    fn expire_and_persist() {
1265        let c = cache();
1266        c.set("k", "v", None);
1267        assert!(c.expire("k", 100));
1268        assert!(c.ttl("k") > 0);
1269        assert!(c.persist("k"));
1270        assert_eq!(c.ttl("k"), -1);
1271    }
1272
1273    #[test]
1274    fn expire_on_missing_key() {
1275        let c = cache();
1276        assert!(!c.expire("nope", 10));
1277    }
1278
1279    // -- Del / Exists -------------------------------------------------------
1280
1281    #[test]
1282    fn del_existing_key() {
1283        let c = cache();
1284        c.set("k", "v", None);
1285        assert!(c.del("k"));
1286        assert!(!c.exists("k"));
1287    }
1288
1289    #[test]
1290    fn del_missing_key() {
1291        let c = cache();
1292        assert!(!c.del("nope"));
1293    }
1294
1295    #[test]
1296    fn exists_key() {
1297        let c = cache();
1298        assert!(!c.exists("k"));
1299        c.set("k", "v", None);
1300        assert!(c.exists("k"));
1301    }
1302
1303    // -- List operations ----------------------------------------------------
1304
1305    #[test]
1306    fn lpush_and_rpush() {
1307        let c = cache();
1308        c.lpush("list", "b");
1309        c.lpush("list", "a");
1310        c.rpush("list", "c");
1311        assert_eq!(
1312            c.lrange("list", 0, -1),
1313            vec!["a".to_string(), "b".to_string(), "c".to_string()]
1314        );
1315    }
1316
1317    #[test]
1318    fn lpop_and_rpop() {
1319        let c = cache();
1320        c.rpush("list", "a");
1321        c.rpush("list", "b");
1322        c.rpush("list", "c");
1323        assert_eq!(c.lpop("list"), Some("a".to_string()));
1324        assert_eq!(c.rpop("list"), Some("c".to_string()));
1325        assert_eq!(c.llen("list"), 1);
1326    }
1327
1328    #[test]
1329    fn lrange_with_negative_indices() {
1330        let c = cache();
1331        for v in &["a", "b", "c", "d", "e"] {
1332            c.rpush("list", v);
1333        }
1334        // Last two elements.
1335        assert_eq!(
1336            c.lrange("list", -2, -1),
1337            vec!["d".to_string(), "e".to_string()]
1338        );
1339    }
1340
1341    #[test]
1342    fn llen_empty_and_missing() {
1343        let c = cache();
1344        assert_eq!(c.llen("nope"), 0);
1345        c.rpush("list", "x");
1346        assert_eq!(c.llen("list"), 1);
1347    }
1348
1349    #[test]
1350    fn lpop_empty_list() {
1351        let c = cache();
1352        assert_eq!(c.lpop("nope"), None);
1353    }
1354
1355    // -- Set operations -----------------------------------------------------
1356
1357    #[test]
1358    fn sadd_and_sismember() {
1359        let c = cache();
1360        assert!(c.sadd("s", "a"));
1361        assert!(!c.sadd("s", "a")); // duplicate
1362        assert!(c.sismember("s", "a"));
1363        assert!(!c.sismember("s", "b"));
1364    }
1365
1366    #[test]
1367    fn srem_member() {
1368        let c = cache();
1369        c.sadd("s", "a");
1370        c.sadd("s", "b");
1371        assert!(c.srem("s", "a"));
1372        assert!(!c.sismember("s", "a"));
1373        assert_eq!(c.scard("s"), 1);
1374    }
1375
1376    #[test]
1377    fn smembers_returns_all() {
1378        let c = cache();
1379        c.sadd("s", "x");
1380        c.sadd("s", "y");
1381        let mut members = c.smembers("s");
1382        members.sort();
1383        assert_eq!(members, vec!["x".to_string(), "y".to_string()]);
1384    }
1385
1386    #[test]
1387    fn scard_and_empty() {
1388        let c = cache();
1389        assert_eq!(c.scard("nope"), 0);
1390        c.sadd("s", "a");
1391        assert_eq!(c.scard("s"), 1);
1392    }
1393
1394    #[test]
1395    fn sinter_two_sets() {
1396        let c = cache();
1397        c.sadd("s1", "a");
1398        c.sadd("s1", "b");
1399        c.sadd("s1", "c");
1400        c.sadd("s2", "b");
1401        c.sadd("s2", "c");
1402        c.sadd("s2", "d");
1403        let mut inter = c.sinter("s1", "s2");
1404        inter.sort();
1405        assert_eq!(inter, vec!["b".to_string(), "c".to_string()]);
1406    }
1407
1408    #[test]
1409    fn sunion_two_sets() {
1410        let c = cache();
1411        c.sadd("s1", "a");
1412        c.sadd("s1", "b");
1413        c.sadd("s2", "b");
1414        c.sadd("s2", "c");
1415        let mut union = c.sunion("s1", "s2");
1416        union.sort();
1417        assert_eq!(
1418            union,
1419            vec!["a".to_string(), "b".to_string(), "c".to_string()]
1420        );
1421    }
1422
1423    // -- Hash operations ----------------------------------------------------
1424
1425    #[test]
1426    fn hset_and_hget() {
1427        let c = cache();
1428        c.hset("h", "name", "alice");
1429        assert_eq!(c.hget("h", "name"), Some("alice".to_string()));
1430        assert_eq!(c.hget("h", "missing"), None);
1431    }
1432
1433    #[test]
1434    fn hdel_field() {
1435        let c = cache();
1436        c.hset("h", "a", "1");
1437        c.hset("h", "b", "2");
1438        assert!(c.hdel("h", "a"));
1439        assert!(!c.hexists("h", "a"));
1440        assert_eq!(c.hlen("h"), 1);
1441    }
1442
1443    #[test]
1444    fn hgetall_returns_map() {
1445        let c = cache();
1446        c.hset("h", "x", "1");
1447        c.hset("h", "y", "2");
1448        let all = c.hgetall("h");
1449        assert_eq!(all.len(), 2);
1450        assert_eq!(all.get("x"), Some(&"1".to_string()));
1451        assert_eq!(all.get("y"), Some(&"2".to_string()));
1452    }
1453
1454    #[test]
1455    fn hexists_and_hlen() {
1456        let c = cache();
1457        assert!(!c.hexists("h", "f"));
1458        assert_eq!(c.hlen("h"), 0);
1459        c.hset("h", "f", "v");
1460        assert!(c.hexists("h", "f"));
1461        assert_eq!(c.hlen("h"), 1);
1462    }
1463
1464    #[test]
1465    fn hkeys_returns_field_names() {
1466        let c = cache();
1467        c.hset("h", "a", "1");
1468        c.hset("h", "b", "2");
1469        let mut keys = c.hkeys("h");
1470        keys.sort();
1471        assert_eq!(keys, vec!["a".to_string(), "b".to_string()]);
1472    }
1473
1474    #[test]
1475    fn hincrby_creates_and_increments() {
1476        let c = cache();
1477        assert_eq!(c.hincrby("h", "count", 5), Ok(5));
1478        assert_eq!(c.hincrby("h", "count", 3), Ok(8));
1479    }
1480
1481    #[test]
1482    fn hincrby_non_integer_errors() {
1483        let c = cache();
1484        c.hset("h", "name", "alice");
1485        assert!(c.hincrby("h", "name", 1).is_err());
1486    }
1487
1488    // -- Sorted set operations ----------------------------------------------
1489
1490    #[test]
1491    fn zadd_and_zscore() {
1492        let c = cache();
1493        c.zadd("z", 1.5, "a");
1494        c.zadd("z", 2.5, "b");
1495        assert_eq!(c.zscore("z", "a"), Some(1.5));
1496        assert_eq!(c.zscore("z", "b"), Some(2.5));
1497        assert_eq!(c.zscore("z", "c"), None);
1498    }
1499
1500    #[test]
1501    fn zrem_member() {
1502        let c = cache();
1503        c.zadd("z", 1.0, "a");
1504        c.zadd("z", 2.0, "b");
1505        assert!(c.zrem("z", "a"));
1506        assert!(!c.zrem("z", "a"));
1507        assert_eq!(c.zcard("z"), 1);
1508    }
1509
1510    #[test]
1511    fn zrank_by_score() {
1512        let c = cache();
1513        c.zadd("z", 3.0, "c");
1514        c.zadd("z", 1.0, "a");
1515        c.zadd("z", 2.0, "b");
1516        assert_eq!(c.zrank("z", "a"), Some(0));
1517        assert_eq!(c.zrank("z", "b"), Some(1));
1518        assert_eq!(c.zrank("z", "c"), Some(2));
1519    }
1520
1521    #[test]
1522    fn zrange_returns_ordered_slice() {
1523        let c = cache();
1524        c.zadd("z", 3.0, "c");
1525        c.zadd("z", 1.0, "a");
1526        c.zadd("z", 2.0, "b");
1527        let range = c.zrange("z", 0, 1);
1528        assert_eq!(range, vec![("a".to_string(), 1.0), ("b".to_string(), 2.0),]);
1529    }
1530
1531    #[test]
1532    fn zcard_empty_and_filled() {
1533        let c = cache();
1534        assert_eq!(c.zcard("z"), 0);
1535        c.zadd("z", 1.0, "x");
1536        assert_eq!(c.zcard("z"), 1);
1537    }
1538
1539    // -- Key pattern matching -----------------------------------------------
1540
1541    #[test]
1542    fn keys_star_pattern() {
1543        let c = cache();
1544        c.set("user:1", "a", None);
1545        c.set("user:2", "b", None);
1546        c.set("post:1", "c", None);
1547        let mut matched = c.keys("user:*");
1548        matched.sort();
1549        assert_eq!(matched, vec!["user:1".to_string(), "user:2".to_string()]);
1550    }
1551
1552    #[test]
1553    fn keys_question_mark_pattern() {
1554        let c = cache();
1555        c.set("a1", "v", None);
1556        c.set("a2", "v", None);
1557        c.set("ab", "v", None);
1558        let mut matched = c.keys("a?");
1559        matched.sort();
1560        assert_eq!(
1561            matched,
1562            vec!["a1".to_string(), "a2".to_string(), "ab".to_string()]
1563        );
1564    }
1565
1566    #[test]
1567    fn keys_all_pattern() {
1568        let c = cache();
1569        c.set("x", "1", None);
1570        c.set("y", "2", None);
1571        assert_eq!(c.keys("*").len(), 2);
1572    }
1573
1574    // -- Type detection -----------------------------------------------------
1575
1576    #[test]
1577    fn key_type_detection() {
1578        let c = cache();
1579        c.set("s", "val", None);
1580        c.rpush("l", "item");
1581        c.sadd("set", "m");
1582        c.hset("h", "f", "v");
1583        c.zadd("z", 1.0, "m");
1584
1585        assert_eq!(c.key_type("s"), Some("string"));
1586        assert_eq!(c.key_type("l"), Some("list"));
1587        assert_eq!(c.key_type("set"), Some("set"));
1588        assert_eq!(c.key_type("h"), Some("hash"));
1589        assert_eq!(c.key_type("z"), Some("zset"));
1590        assert_eq!(c.key_type("nope"), None);
1591    }
1592
1593    // -- Eviction -----------------------------------------------------------
1594
1595    #[test]
1596    fn lru_eviction_when_over_max_keys() {
1597        let c = CachePlugin::new(3);
1598        c.set("a", "1", None);
1599        c.set("b", "2", None);
1600        c.set("c", "3", None);
1601
1602        // Access "a" so it becomes most-recently-used.
1603        c.get("a");
1604
1605        // Adding a 4th key should evict the LRU key ("b" was set after "a"
1606        // but never accessed again, and "c" was set most recently).
1607        // Actually "b" has the oldest last_accessed because set() creates
1608        // entries with last_accessed = now, but "a" was accessed after "b"
1609        // and "c" was set after "b". So "b" should be evicted.
1610        c.set("d", "4", None);
1611
1612        assert_eq!(c.dbsize(), 3);
1613        assert!(c.exists("a")); // was accessed, should survive
1614        assert!(!c.exists("b")); // LRU, should be evicted
1615        assert!(c.exists("c"));
1616        assert!(c.exists("d"));
1617
1618        let stats = c.info();
1619        assert!(stats.evictions >= 1);
1620    }
1621
1622    // -- Stats --------------------------------------------------------------
1623
1624    #[test]
1625    fn stats_hit_miss_tracking() {
1626        let c = cache();
1627        c.set("k", "v", None);
1628        c.get("k"); // hit
1629        c.get("k"); // hit
1630        c.get("missing"); // miss
1631
1632        let stats = c.info();
1633        assert_eq!(stats.hits, 2);
1634        assert_eq!(stats.misses, 1);
1635        assert_eq!(stats.sets, 1);
1636    }
1637
1638    // -- Cleanup ------------------------------------------------------------
1639
1640    #[test]
1641    fn cleanup_expired_keys() {
1642        let c = cache();
1643        c.set("keep", "yes", None);
1644        c.set("expire1", "no", Some(0));
1645        c.set("expire2", "no", Some(0));
1646        thread::sleep(Duration::from_millis(5));
1647
1648        let removed = c.cleanup_expired();
1649        assert_eq!(removed, 2);
1650        assert!(c.exists("keep"));
1651        assert!(!c.exists("expire1"));
1652        assert!(!c.exists("expire2"));
1653    }
1654
1655    // -- DBSIZE / FLUSHALL --------------------------------------------------
1656
1657    #[test]
1658    fn dbsize_counts_non_expired() {
1659        let c = cache();
1660        c.set("a", "1", None);
1661        c.set("b", "2", None);
1662        c.set("c", "3", Some(0));
1663        thread::sleep(Duration::from_millis(5));
1664        // "c" is expired so dbsize should be 2.
1665        assert_eq!(c.dbsize(), 2);
1666    }
1667
1668    #[test]
1669    fn flushall_clears_everything() {
1670        let c = cache();
1671        c.set("a", "1", None);
1672        c.set("b", "2", None);
1673        c.rpush("list", "x");
1674        c.flushall();
1675        assert_eq!(c.dbsize(), 0);
1676        let stats = c.info();
1677        assert_eq!(stats.sets, 0);
1678    }
1679
1680    // -- Plugin trait -------------------------------------------------------
1681
1682    #[test]
1683    fn plugin_name() {
1684        let c = cache();
1685        assert_eq!(Plugin::name(&c), "cache");
1686    }
1687
1688    // -- Glob matching unit tests -------------------------------------------
1689
1690    #[test]
1691    fn glob_match_exact() {
1692        assert!(glob_match("hello", "hello"));
1693        assert!(!glob_match("hello", "world"));
1694    }
1695
1696    #[test]
1697    fn glob_match_star() {
1698        assert!(glob_match("h*o", "hello"));
1699        assert!(glob_match("*", "anything"));
1700        assert!(glob_match("pre*", "prefix"));
1701        assert!(glob_match("*fix", "suffix"));
1702    }
1703
1704    #[test]
1705    fn glob_match_question() {
1706        assert!(glob_match("h?llo", "hello"));
1707        assert!(!glob_match("h?llo", "hllo"));
1708    }
1709}