1use crate::cache::Cache;
50use crate::error::CacheError;
51use crate::value::Value;
52use std::collections::HashMap;
53use std::future::Future;
54use std::pin::Pin;
55use std::sync::{Arc, RwLock};
56use std::time::Duration;
57use tokio::time::Instant;
62
63#[derive(Debug, Clone)]
69pub enum InvalidationMessage {
70 InvalidateKey(String),
72 InvalidateTable(String),
74 InvalidateAll,
76}
77
78pub trait InvalidationBus: Send + Sync {
83 fn publish(&self, message: InvalidationMessage);
85 fn subscribe(&self) -> Box<dyn Iterator<Item = InvalidationMessage> + Send>;
87}
88
89pub struct LocalInvalidationBus {
94 tx: tokio::sync::broadcast::Sender<InvalidationMessage>,
95}
96
97impl LocalInvalidationBus {
98 pub fn new(capacity: usize) -> Self {
100 let (tx, _rx) = tokio::sync::broadcast::channel(capacity.max(1));
101 Self { tx }
102 }
103}
104
105impl Default for LocalInvalidationBus {
106 fn default() -> Self {
107 Self::new(256)
108 }
109}
110
111impl InvalidationBus for LocalInvalidationBus {
112 fn publish(&self, message: InvalidationMessage) {
113 let _ = self.tx.send(message);
115 }
116
117 fn subscribe(&self) -> Box<dyn Iterator<Item = InvalidationMessage> + Send> {
118 let mut rx = self.tx.subscribe();
119 Box::new(std::iter::from_fn(move || loop {
120 match rx.try_recv() {
121 Ok(msg) => return Some(msg),
122 Err(tokio::sync::broadcast::error::TryRecvError::Empty)
124 | Err(tokio::sync::broadcast::error::TryRecvError::Closed) => return None,
125 Err(tokio::sync::broadcast::error::TryRecvError::Lagged(_)) => continue,
127 }
128 }))
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Hash)]
143pub struct CacheKey {
144 pub table: String,
146 pub kind: CacheKeyKind,
148 pub identifier: String,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Hash)]
154pub enum CacheKeyKind {
155 ByPk,
157 ByQuery,
159 ByRelation,
161}
162
163impl CacheKey {
164 pub fn by_pk(table: impl Into<String>, pk: impl std::fmt::Display) -> Self {
166 Self {
167 table: table.into(),
168 kind: CacheKeyKind::ByPk,
169 identifier: pk.to_string(),
170 }
171 }
172
173 pub fn by_query(table: impl Into<String>, query_hash: impl std::fmt::Display) -> Self {
175 Self {
176 table: table.into(),
177 kind: CacheKeyKind::ByQuery,
178 identifier: query_hash.to_string(),
179 }
180 }
181
182 pub fn by_relation(table: impl Into<String>, relation: impl std::fmt::Display) -> Self {
184 Self {
185 table: table.into(),
186 kind: CacheKeyKind::ByRelation,
187 identifier: relation.to_string(),
188 }
189 }
190
191 pub fn to_string_key(&self) -> String {
193 let kind_str = match self.kind {
194 CacheKeyKind::ByPk => "pk",
195 CacheKeyKind::ByQuery => "q",
196 CacheKeyKind::ByRelation => "rel",
197 };
198 format!("l2:{}:{}:{}", self.table, kind_str, self.identifier)
199 }
200}
201
202impl std::fmt::Display for CacheKey {
203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204 write!(f, "{}", self.to_string_key())
205 }
206}
207
208#[derive(Debug, Clone, Default)]
214pub struct L2CacheStats {
215 pub hits: u64,
217 pub misses: u64,
219 pub sets: u64,
221 pub evictions: u64,
223 pub size: usize,
225}
226
227#[derive(Debug, Clone, Default)]
232pub struct PerTableStats {
233 pub hits: u64,
235 pub misses: u64,
237 pub sets: u64,
239 pub evictions: u64,
241}
242
243impl PerTableStats {
244 pub fn total_lookups(&self) -> u64 {
246 self.hits + self.misses
247 }
248
249 pub fn hit_rate(&self) -> f64 {
251 let total = self.total_lookups();
252 if total == 0 {
253 0.0
254 } else {
255 self.hits as f64 / total as f64
256 }
257 }
258}
259
260impl L2CacheStats {
261 pub fn total_lookups(&self) -> u64 {
263 self.hits + self.misses
264 }
265
266 pub fn hit_rate(&self) -> f64 {
268 let total = self.total_lookups();
269 if total == 0 {
270 0.0
271 } else {
272 self.hits as f64 / total as f64
273 }
274 }
275
276 pub fn miss_rate(&self) -> f64 {
278 1.0 - self.hit_rate()
279 }
280
281 pub fn merge(&mut self, other: &L2CacheStats) {
283 self.hits += other.hits;
284 self.misses += other.misses;
285 self.sets += other.sets;
286 self.evictions += other.evictions;
287 self.size += other.size;
288 }
289}
290
291#[derive(Debug, Clone)]
297struct CacheEntry {
298 value: Value,
300 expires_at: Option<Instant>,
302}
303
304impl CacheEntry {
305 fn new(value: Value, ttl: Option<Duration>) -> Self {
306 let expires_at = ttl.and_then(|d| {
309 if d == Duration::MAX {
310 None
311 } else {
312 Some(Instant::now() + d)
313 }
314 });
315 Self { value, expires_at }
316 }
317
318 fn is_expired(&self) -> bool {
319 self.expires_at
320 .map(|t| t <= Instant::now())
321 .unwrap_or(false)
322 }
323}
324
325struct LruOrder {
340 nodes: Vec<LruNode>,
342 free_list: Vec<usize>,
344 index: HashMap<String, usize>,
346 head: Option<usize>,
348 tail: Option<usize>,
350}
351
352struct LruNode {
354 key: String,
355 prev: Option<usize>,
356 next: Option<usize>,
357}
358
359impl LruOrder {
360 fn new() -> Self {
361 Self {
362 nodes: Vec::new(),
363 free_list: Vec::new(),
364 index: HashMap::new(),
365 head: None,
366 tail: None,
367 }
368 }
369
370 fn touch(&mut self, key: &str) {
372 if let Some(&idx) = self.index.get(key) {
373 self.unlink(idx);
374 self.link_tail(idx);
375 } else {
376 let idx = self.alloc_node(key.to_string());
377 self.link_tail(idx);
378 self.index.insert(key.to_string(), idx);
379 }
380 }
381
382 fn remove(&mut self, key: &str) {
384 if let Some(idx) = self.index.remove(key) {
385 self.unlink(idx);
386 self.free_node(idx);
387 }
388 }
389
390 fn lru_key(&self) -> Option<&str> {
392 self.head.map(|idx| self.nodes[idx].key.as_str())
393 }
394
395 fn iter_keys(&self) -> impl Iterator<Item = &str> {
397 LruIter {
398 nodes: &self.nodes,
399 current: self.head,
400 }
401 }
402
403 fn clear(&mut self) {
405 self.nodes.clear();
406 self.free_list.clear();
407 self.index.clear();
408 self.head = None;
409 self.tail = None;
410 }
411
412 #[allow(dead_code)]
414 fn len(&self) -> usize {
415 self.index.len()
416 }
417
418 fn alloc_node(&mut self, key: String) -> usize {
420 if let Some(idx) = self.free_list.pop() {
421 self.nodes[idx] = LruNode {
422 key,
423 prev: None,
424 next: None,
425 };
426 idx
427 } else {
428 self.nodes.push(LruNode {
429 key,
430 prev: None,
431 next: None,
432 });
433 self.nodes.len() - 1
434 }
435 }
436
437 fn free_node(&mut self, idx: usize) {
439 self.free_list.push(idx);
440 }
441
442 fn unlink(&mut self, idx: usize) {
444 let prev = self.nodes[idx].prev;
445 let next = self.nodes[idx].next;
446 match prev {
447 Some(p) => self.nodes[p].next = next,
448 None => self.head = next,
449 }
450 match next {
451 Some(n) => self.nodes[n].prev = prev,
452 None => self.tail = prev,
453 }
454 self.nodes[idx].prev = None;
455 self.nodes[idx].next = None;
456 }
457
458 fn link_tail(&mut self, idx: usize) {
460 match self.tail {
461 Some(t) => {
462 self.nodes[t].next = Some(idx);
463 self.nodes[idx].prev = Some(t);
464 }
465 None => self.head = Some(idx),
466 }
467 self.nodes[idx].next = None;
468 self.tail = Some(idx);
469 }
470}
471
472struct LruIter<'a> {
474 nodes: &'a [LruNode],
475 current: Option<usize>,
476}
477
478impl<'a> Iterator for LruIter<'a> {
479 type Item = &'a str;
480
481 fn next(&mut self) -> Option<Self::Item> {
482 let idx = self.current?;
483 let node = &self.nodes[idx];
484 self.current = node.next;
485 Some(node.key.as_str())
486 }
487}
488
489pub struct L2Cache {
518 data: RwLock<HashMap<String, CacheEntry>>,
520 table_index: RwLock<HashMap<String, Vec<String>>>,
522 access_order: RwLock<LruOrder>,
529 stats: RwLock<L2CacheStats>,
531 table_stats: RwLock<HashMap<String, PerTableStats>>,
533 default_ttl: Option<Duration>,
535 max_size: usize,
537 invalidation_bus: Option<Arc<dyn InvalidationBus>>,
539}
540
541impl Default for L2Cache {
542 fn default() -> Self {
543 Self::new()
544 }
545}
546
547impl L2Cache {
548 pub fn new() -> Self {
550 Self {
551 data: RwLock::new(HashMap::new()),
552 table_index: RwLock::new(HashMap::new()),
553 access_order: RwLock::new(LruOrder::new()),
554 stats: RwLock::new(L2CacheStats::default()),
555 table_stats: RwLock::new(HashMap::new()),
556 default_ttl: None,
557 max_size: 10_000,
558 invalidation_bus: None,
559 }
560 }
561
562 pub fn with_default_ttl(mut self, ttl: Duration) -> Self {
564 self.default_ttl = Some(ttl);
565 self
566 }
567
568 pub fn with_max_size(mut self, max_size: usize) -> Self {
570 self.max_size = max_size;
571 self
572 }
573
574 pub fn with_invalidation_bus(mut self, bus: Arc<dyn InvalidationBus>) -> Self {
576 self.invalidation_bus = Some(bus);
577 self
578 }
579
580 pub fn put(&self, key: &CacheKey, value: Value, ttl: Option<Duration>) {
588 let actual_ttl = ttl.or(self.default_ttl);
589 let entry = CacheEntry::new(value, actual_ttl);
590 let key_str = key.to_string_key();
591
592 {
594 let mut data = self.data.write().expect("L2Cache data lock poisoned (put)");
595 let exists = data.contains_key(&key_str);
596 if !exists && data.len() >= self.max_size {
597 let victim = {
599 let order = self
601 .access_order
602 .read()
603 .expect("L2Cache access_order lock poisoned (put-victim-read)");
604 let expired = order
607 .iter_keys()
608 .find(|k| data.get(*k).map(|e| e.is_expired()).unwrap_or(false))
609 .map(|s| s.to_string());
610 let lru = order.lru_key().map(|s| s.to_string());
611 expired.or(lru)
612 };
613 if let Some(victim) = victim {
614 data.remove(&victim);
615 let mut order = self
617 .access_order
618 .write()
619 .expect("L2Cache access_order lock poisoned (put-victim-remove)");
620 order.remove(&victim);
621 }
622 }
623 data.insert(key_str.clone(), entry);
624 };
625
626 {
628 let mut order = self
629 .access_order
630 .write()
631 .expect("L2Cache access_order lock poisoned (put-touch)");
632 order.touch(&key_str);
633 }
634
635 {
637 let mut idx = self
638 .table_index
639 .write()
640 .expect("L2Cache table_index lock poisoned (put)");
641 let keys = idx.entry(key.table.clone()).or_default();
642 if !keys.contains(&key_str) {
643 keys.push(key_str);
644 }
645 }
646
647 {
649 let mut stats = self
650 .stats
651 .write()
652 .expect("L2Cache stats lock poisoned (put)");
653 stats.sets += 1;
654 }
655 {
657 if let Ok(mut tbl_stats) = self.table_stats.write() {
658 tbl_stats.entry(key.table.clone()).or_default().sets += 1;
659 }
660 }
661 }
662
663 pub fn get(&self, key: &CacheKey) -> Option<Value> {
667 let key_str = key.to_string_key();
668 let table_name = key.table.clone();
669 let result = {
670 let data = self.data.read().ok()?;
671 if let Some(entry) = data.get(&key_str) {
672 if entry.is_expired() {
673 None
674 } else {
675 Some(entry.value.clone())
676 }
677 } else {
678 None
679 }
680 };
681
682 if result.is_some() {
684 let mut order = self
685 .access_order
686 .write()
687 .expect("L2Cache access_order lock poisoned (get)");
688 order.touch(&key_str);
689 }
690
691 if let Ok(mut stats) = self.stats.write() {
693 if result.is_some() {
694 stats.hits += 1;
695 } else {
696 stats.misses += 1;
697 }
698 }
699 if let Ok(mut tbl_stats) = self.table_stats.write() {
701 let entry = tbl_stats.entry(table_name).or_default();
702 if result.is_some() {
703 entry.hits += 1;
704 } else {
705 entry.misses += 1;
706 }
707 }
708
709 result
710 }
711
712 pub fn invalidate(&self, key: &CacheKey) {
714 let key_str = key.to_string_key();
715 let table_name = key.table.clone();
716 let removed = {
717 let mut data = self
718 .data
719 .write()
720 .expect("L2Cache data lock poisoned (invalidate)");
721 data.remove(&key_str).is_some()
722 };
723 if removed {
724 let mut order = self
725 .access_order
726 .write()
727 .expect("L2Cache access_order lock poisoned (invalidate)");
728 order.remove(&key_str);
729 }
730 if removed {
731 let mut stats = self
732 .stats
733 .write()
734 .expect("L2Cache stats lock poisoned (invalidate)");
735 stats.evictions += 1;
736 if let Ok(mut tbl_stats) = self.table_stats.write() {
737 tbl_stats.entry(table_name).or_default().evictions += 1;
738 }
739 }
740 }
741
742 pub fn invalidate_table(&self, table: &str) {
747 let keys_to_remove: Vec<String> = {
748 let idx = match self.table_index.read() {
749 Ok(i) => i,
750 Err(_) => return,
751 };
752 idx.get(table).cloned().unwrap_or_default()
753 };
754
755 let mut actually_removed: usize = 0;
756 {
757 let mut data = self
758 .data
759 .write()
760 .expect("L2Cache data lock poisoned (invalidate_table)");
761 for k in &keys_to_remove {
762 if data.remove(k).is_some() {
763 actually_removed += 1;
764 }
765 }
766 }
767
768 if actually_removed > 0 {
770 let mut order = self
771 .access_order
772 .write()
773 .expect("L2Cache access_order lock poisoned (invalidate_table)");
774 for k in &keys_to_remove {
775 order.remove(k);
776 }
777 }
778
779 if let Ok(mut idx) = self.table_index.write() {
780 idx.remove(table);
781 }
782 if actually_removed > 0 {
783 let mut stats = self
784 .stats
785 .write()
786 .expect("L2Cache stats lock poisoned (invalidate_table)");
787 stats.evictions += actually_removed as u64;
788 if let Ok(mut tbl_stats) = self.table_stats.write() {
789 tbl_stats.entry(table.to_string()).or_default().evictions +=
790 actually_removed as u64;
791 }
792 }
793
794 if let Some(bus) = &self.invalidation_bus {
796 bus.publish(InvalidationMessage::InvalidateTable(table.to_string()));
797 }
798 }
799
800 pub fn clear(&self) {
802 let removed = {
803 let mut data = self
804 .data
805 .write()
806 .expect("L2Cache data lock poisoned (clear)");
807 let n = data.len();
808 data.clear();
809 n
810 };
811 if let Ok(mut order) = self.access_order.write() {
812 order.clear();
813 }
814 if let Ok(mut idx) = self.table_index.write() {
815 idx.clear();
816 }
817 if let Ok(mut tbl_stats) = self.table_stats.write() {
818 tbl_stats.clear();
819 }
820 if removed > 0 {
821 let mut stats = self
822 .stats
823 .write()
824 .expect("L2Cache stats lock poisoned (clear)");
825 stats.evictions += removed as u64;
826 stats.size = 0;
827 }
828 }
829
830 pub fn size(&self) -> usize {
832 self.data.read().map(|d| d.len()).unwrap_or(0)
833 }
834
835 pub fn stats(&self) -> L2CacheStats {
837 let mut s = self.stats.read().map(|s| s.clone()).unwrap_or_default();
838 s.size = self.size();
840 s
841 }
842
843 pub fn reset_stats(&self) {
845 if let Ok(mut stats) = self.stats.write() {
846 *stats = L2CacheStats::default();
847 }
848 if let Ok(mut tbl_stats) = self.table_stats.write() {
849 tbl_stats.clear();
850 }
851 }
852
853 pub fn table_stats(&self, table: &str) -> Option<PerTableStats> {
855 self.table_stats
856 .read()
857 .ok()
858 .and_then(|s| s.get(table).cloned())
859 }
860
861 pub fn all_table_stats(&self) -> HashMap<String, PerTableStats> {
863 self.table_stats
864 .read()
865 .map(|s| s.clone())
866 .unwrap_or_default()
867 }
868
869 pub fn contains(&self, key: &CacheKey) -> bool {
871 let key_str = key.to_string_key();
872 self.data
873 .read()
874 .map(|d| d.get(&key_str).map(|e| !e.is_expired()).unwrap_or(false))
875 .unwrap_or(false)
876 }
877
878 pub fn evict_expired(&self) -> usize {
880 let expired_keys: Vec<String> = {
881 let data = self
882 .data
883 .read()
884 .expect("L2Cache data lock poisoned (evict_expired-read)");
885 data.iter()
886 .filter(|(_, e)| e.is_expired())
887 .map(|(k, _)| k.clone())
888 .collect()
889 };
890
891 let key_to_table: HashMap<String, String> = {
893 let idx = self
894 .table_index
895 .read()
896 .expect("L2Cache table_index lock poisoned (evict_expired-idx)");
897 let mut map = HashMap::new();
898 for (table, keys) in idx.iter() {
899 for k in keys {
900 map.insert(k.clone(), table.clone());
901 }
902 }
903 map
904 };
905
906 let mut removed = 0;
907 if !expired_keys.is_empty() {
908 let mut data = self
909 .data
910 .write()
911 .expect("L2Cache data lock poisoned (evict_expired-write)");
912 for k in &expired_keys {
913 if data.remove(k).is_some() {
914 removed += 1;
915 }
916 }
917 }
918
919 if removed > 0 {
920 let mut order = self
922 .access_order
923 .write()
924 .expect("L2Cache access_order lock poisoned (evict_expired)");
925 for k in &expired_keys {
926 order.remove(k);
927 }
928 {
929 let mut stats = self
930 .stats
931 .write()
932 .expect("L2Cache stats lock poisoned (evict_expired)");
933 stats.evictions += removed as u64;
934 }
935 if let Ok(mut tbl_stats) = self.table_stats.write() {
937 for k in &expired_keys {
938 if let Some(table) = key_to_table.get(k) {
939 tbl_stats.entry(table.clone()).or_default().evictions += 1;
940 }
941 }
942 }
943 }
944 removed
945 }
946
947 pub fn update_ttl(&self, key: &CacheKey, ttl: Duration) -> bool {
951 let key_str = key.to_string_key();
952 let mut data = match self.data.write() {
953 Ok(d) => d,
954 Err(_) => return false,
955 };
956 if let Some(entry) = data.get_mut(&key_str) {
957 entry.expires_at = Some(Instant::now() + ttl);
958 true
959 } else {
960 false
961 }
962 }
963
964 pub fn remaining_ttl(&self, key: &CacheKey) -> Option<Option<Duration>> {
973 let key_str = key.to_string_key();
974 let data = self.data.read().ok()?;
975 let entry = data.get(&key_str)?;
976 match entry.expires_at {
977 Some(expires_at) => {
978 let now = Instant::now();
979 if expires_at <= now {
980 None
981 } else {
982 Some(Some(expires_at.duration_since(now)))
983 }
984 }
985 None => Some(None),
986 }
987 }
988}
989
990impl Cache for L2Cache {
1002 fn get(&self, key: &str) -> Result<Option<Vec<u8>>, CacheError> {
1003 let cache_key = CacheKey::by_pk("__cache__", key);
1004 match L2Cache::get(self, &cache_key) {
1005 Some(Value::Bytes(bytes)) => Ok(Some(bytes)),
1006 Some(other) => {
1007 let json = serde_json::to_vec(&other)
1008 .map_err(|e| CacheError::SerializationError(e.to_string()))?;
1009 Ok(Some(json))
1010 }
1011 None => Ok(None),
1012 }
1013 }
1014
1015 fn set(&self, key: &str, value: Vec<u8>, ttl: Option<Duration>) -> Result<(), CacheError> {
1016 let cache_key = CacheKey::by_pk("__cache__", key);
1017 self.put(&cache_key, Value::Bytes(value), ttl);
1018 Ok(())
1019 }
1020
1021 fn delete(&self, key: &str) -> Result<(), CacheError> {
1022 let cache_key = CacheKey::by_pk("__cache__", key);
1023 self.invalidate(&cache_key);
1024 Ok(())
1025 }
1026
1027 fn clear(&self) -> Result<(), CacheError> {
1028 self.invalidate_table("__cache__");
1031 Ok(())
1032 }
1033
1034 fn exists(&self, key: &str) -> Result<bool, CacheError> {
1035 let cache_key = CacheKey::by_pk("__cache__", key);
1036 Ok(self.contains(&cache_key))
1037 }
1038
1039 fn expire(&self, key: &str, ttl: Duration) -> Result<(), CacheError> {
1040 let cache_key = CacheKey::by_pk("__cache__", key);
1041 if self.update_ttl(&cache_key, ttl) {
1042 Ok(())
1043 } else {
1044 Err(CacheError::NotFound(key.to_string()))
1045 }
1046 }
1047
1048 fn ttl(&self, key: &str) -> Result<Option<Duration>, CacheError> {
1049 let cache_key = CacheKey::by_pk("__cache__", key);
1050 match self.remaining_ttl(&cache_key) {
1051 None => Err(CacheError::NotFound(key.to_string())),
1052 Some(None) => Ok(None),
1053 Some(Some(d)) => Ok(Some(d)),
1054 }
1055 }
1056}
1057
1058pub type L2CacheFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, CacheError>> + Send + 'a>>;
1067
1068pub trait L2CacheBackend: Send + Sync {
1084 fn get<'a>(&'a self, key: &'a str) -> L2CacheFuture<'a, Option<Vec<u8>>>;
1086
1087 fn set<'a>(
1089 &'a self,
1090 key: &'a str,
1091 value: &'a [u8],
1092 ttl: Option<Duration>,
1093 ) -> L2CacheFuture<'a, ()>;
1094
1095 fn delete<'a>(&'a self, key: &'a str) -> L2CacheFuture<'a, ()>;
1097
1098 fn invalidate_prefix<'a>(&'a self, prefix: &'a str) -> L2CacheFuture<'a, ()>;
1100}
1101
1102pub struct InMemoryBackend {
1116 data: RwLock<InMemoryCacheData>,
1119}
1120
1121type InMemoryCacheData = HashMap<String, (Vec<u8>, Option<Instant>)>;
1123
1124impl Default for InMemoryBackend {
1125 fn default() -> Self {
1126 Self::new()
1127 }
1128}
1129
1130impl InMemoryBackend {
1131 pub fn new() -> Self {
1133 Self {
1134 data: RwLock::new(HashMap::new()),
1135 }
1136 }
1137}
1138
1139impl L2CacheBackend for InMemoryBackend {
1140 fn get<'a>(&'a self, key: &'a str) -> L2CacheFuture<'a, Option<Vec<u8>>> {
1141 let result = {
1143 let data = match self.data.read() {
1144 Ok(d) => d,
1145 Err(e) => {
1146 let err = CacheError::from(e);
1147 return Box::pin(async move { Err(err) });
1148 }
1149 };
1150 match data.get(key) {
1151 Some((value, expiry)) => {
1152 if expiry.map(|t| t <= Instant::now()).unwrap_or(false) {
1154 Ok(None)
1155 } else {
1156 Ok(Some(value.clone()))
1157 }
1158 }
1159 None => Ok(None),
1160 }
1161 };
1162 Box::pin(async move { result })
1163 }
1164
1165 fn set<'a>(
1166 &'a self,
1167 key: &'a str,
1168 value: &'a [u8],
1169 ttl: Option<Duration>,
1170 ) -> L2CacheFuture<'a, ()> {
1171 let result = {
1172 let mut data = match self.data.write() {
1173 Ok(d) => d,
1174 Err(e) => {
1175 let err = CacheError::from(e);
1176 return Box::pin(async move { Err(err) });
1177 }
1178 };
1179 let expiry = ttl.map(|d| Instant::now() + d);
1180 data.insert(key.to_string(), (value.to_vec(), expiry));
1181 Ok(())
1182 };
1183 Box::pin(async move { result })
1184 }
1185
1186 fn delete<'a>(&'a self, key: &'a str) -> L2CacheFuture<'a, ()> {
1187 let result = {
1188 let mut data = match self.data.write() {
1189 Ok(d) => d,
1190 Err(e) => {
1191 let err = CacheError::from(e);
1192 return Box::pin(async move { Err(err) });
1193 }
1194 };
1195 data.remove(key);
1196 Ok(())
1197 };
1198 Box::pin(async move { result })
1199 }
1200
1201 fn invalidate_prefix<'a>(&'a self, prefix: &'a str) -> L2CacheFuture<'a, ()> {
1202 let result = {
1203 let mut data = match self.data.write() {
1204 Ok(d) => d,
1205 Err(e) => {
1206 let err = CacheError::from(e);
1207 return Box::pin(async move { Err(err) });
1208 }
1209 };
1210 let keys_to_remove: Vec<String> = data
1212 .keys()
1213 .filter(|k| k.starts_with(prefix))
1214 .cloned()
1215 .collect();
1216 for k in keys_to_remove {
1217 data.remove(&k);
1218 }
1219 Ok(())
1220 };
1221 Box::pin(async move { result })
1222 }
1223}
1224
1225#[cfg(feature = "redis")]
1268pub struct RedisBackend {
1269 manager: redis::aio::ConnectionManager,
1271}
1272
1273#[cfg(feature = "redis")]
1274impl RedisBackend {
1275 pub async fn new(url: impl Into<String>) -> Result<Self, CacheError> {
1285 let url = url.into();
1286 let client = redis::Client::open(url.as_str())
1287 .map_err(|e| CacheError::Internal(format!("Redis client create failed: {}", e)))?;
1288 let manager = redis::aio::ConnectionManager::new(client)
1289 .await
1290 .map_err(|e| CacheError::Internal(format!("Redis connect failed: {}", e)))?;
1291 Ok(Self { manager })
1292 }
1293
1294 pub fn from_manager(manager: redis::aio::ConnectionManager) -> Self {
1296 Self { manager }
1297 }
1298
1299 async fn invalidate_prefix_inner(&self, prefix: &str) -> Result<(), CacheError> {
1313 let pattern = format!("{}*", prefix);
1314 let mut cursor: u64 = 0;
1315 loop {
1316 let mut conn = self.manager.clone();
1319 let scan_result: redis::RedisResult<(u64, Vec<String>)> = redis::cmd("SCAN")
1320 .arg(cursor)
1321 .arg("MATCH")
1322 .arg(&pattern)
1323 .arg("COUNT")
1324 .arg(100usize)
1325 .query_async(&mut conn)
1326 .await;
1327 let (next_cursor, keys): (u64, Vec<String>) = scan_result
1328 .map_err(|e| CacheError::Internal(format!("Redis SCAN failed: {}", e)))?;
1329
1330 if !keys.is_empty() {
1331 let mut pipe = redis::pipe();
1333 for k in &keys {
1334 pipe.del(k);
1335 }
1336 let del_result: redis::RedisResult<()> = pipe.query_async(&mut conn).await;
1338 del_result.map_err(|e| {
1339 CacheError::Internal(format!("Redis DEL pipeline failed: {}", e))
1340 })?;
1341 }
1342
1343 if next_cursor == 0 {
1345 break;
1346 }
1347 cursor = next_cursor;
1348 }
1349 Ok(())
1350 }
1351}
1352
1353#[cfg(feature = "redis")]
1354impl L2CacheBackend for RedisBackend {
1355 fn get<'a>(&'a self, key: &'a str) -> L2CacheFuture<'a, Option<Vec<u8>>> {
1356 Box::pin(async move {
1357 use redis::AsyncCommands;
1358 let mut conn = self.manager.clone();
1359 let value: Option<Vec<u8>> = conn
1360 .get(key)
1361 .await
1362 .map_err(|e| CacheError::Internal(format!("Redis GET failed: {}", e)))?;
1363 Ok(value)
1364 })
1365 }
1366
1367 fn set<'a>(
1368 &'a self,
1369 key: &'a str,
1370 value: &'a [u8],
1371 ttl: Option<Duration>,
1372 ) -> L2CacheFuture<'a, ()> {
1373 Box::pin(async move {
1374 use redis::AsyncCommands;
1375 let mut conn = self.manager.clone();
1376 match ttl {
1379 Some(d) => {
1380 let secs = d.as_secs();
1381 if secs > 0 {
1382 let _: () = conn.set_ex(key, value, secs).await.map_err(|e| {
1383 CacheError::Internal(format!("Redis SET EX failed: {}", e))
1384 })?;
1385 } else {
1386 let _: () = conn.set(key, value).await.map_err(|e| {
1388 CacheError::Internal(format!("Redis SET failed: {}", e))
1389 })?;
1390 let ms: i64 = d.as_millis().min(i64::MAX as u128) as i64;
1392 let _: () = conn.pexpire(key, ms).await.map_err(|e| {
1393 CacheError::Internal(format!("Redis PEXPIRE failed: {}", e))
1394 })?;
1395 }
1396 }
1397 None => {
1398 let _: () = conn
1399 .set(key, value)
1400 .await
1401 .map_err(|e| CacheError::Internal(format!("Redis SET failed: {}", e)))?;
1402 }
1403 }
1404 Ok(())
1405 })
1406 }
1407
1408 fn delete<'a>(&'a self, key: &'a str) -> L2CacheFuture<'a, ()> {
1409 Box::pin(async move {
1410 use redis::AsyncCommands;
1411 let mut conn = self.manager.clone();
1412 let _: () = conn
1413 .del(key)
1414 .await
1415 .map_err(|e| CacheError::Internal(format!("Redis DEL failed: {}", e)))?;
1416 Ok(())
1417 })
1418 }
1419
1420 fn invalidate_prefix<'a>(&'a self, prefix: &'a str) -> L2CacheFuture<'a, ()> {
1421 Box::pin(async move { self.invalidate_prefix_inner(prefix).await })
1422 }
1423}
1424
1425#[cfg(not(feature = "redis"))]
1430pub struct RedisBackend {
1431 url: String,
1433}
1434
1435#[cfg(not(feature = "redis"))]
1436impl RedisBackend {
1437 pub fn new(_url: impl Into<String>) -> Self {
1442 Self { url: _url.into() }
1443 }
1444}
1445
1446#[cfg(not(feature = "redis"))]
1447impl L2CacheBackend for RedisBackend {
1448 fn get<'a>(&'a self, _key: &'a str) -> L2CacheFuture<'a, Option<Vec<u8>>> {
1449 let url = self.url.clone();
1450 Box::pin(async move {
1451 Err(CacheError::Internal(format!(
1452 "RedisBackend not compiled: enable 'redis' feature in sz-orm-core. URL: {}",
1453 url
1454 )))
1455 })
1456 }
1457
1458 fn set<'a>(
1459 &'a self,
1460 _key: &'a str,
1461 _value: &'a [u8],
1462 _ttl: Option<Duration>,
1463 ) -> L2CacheFuture<'a, ()> {
1464 let url = self.url.clone();
1465 Box::pin(async move {
1466 Err(CacheError::Internal(format!(
1467 "RedisBackend not compiled: enable 'redis' feature in sz-orm-core. URL: {}",
1468 url
1469 )))
1470 })
1471 }
1472
1473 fn delete<'a>(&'a self, _key: &'a str) -> L2CacheFuture<'a, ()> {
1474 let url = self.url.clone();
1475 Box::pin(async move {
1476 Err(CacheError::Internal(format!(
1477 "RedisBackend not compiled: enable 'redis' feature in sz-orm-core. URL: {}",
1478 url
1479 )))
1480 })
1481 }
1482
1483 fn invalidate_prefix<'a>(&'a self, _prefix: &'a str) -> L2CacheFuture<'a, ()> {
1484 let url = self.url.clone();
1485 Box::pin(async move {
1486 Err(CacheError::Internal(format!(
1487 "RedisBackend not compiled: enable 'redis' feature in sz-orm-core. URL: {}",
1488 url
1489 )))
1490 })
1491 }
1492}
1493
1494#[derive(Debug, Clone)]
1521pub enum WriteOp {
1522 Set {
1524 key: String,
1526 value: Vec<u8>,
1528 ttl: Option<Duration>,
1530 },
1531 Delete {
1533 key: String,
1535 },
1536}
1537
1538pub type FlushCallback = Arc<
1543 dyn Fn(Vec<WriteOp>) -> Pin<Box<dyn Future<Output = Result<(), CacheError>> + Send>>
1544 + Send
1545 + Sync,
1546>;
1547
1548pub type ErrorCallback = Arc<dyn Fn(Vec<WriteOp>, CacheError) + Send + Sync>;
1550
1551pub struct WriteBehindWriter {
1590 backend: Arc<dyn L2CacheBackend>,
1592 queue: tokio::sync::Mutex<Vec<WriteOp>>,
1594 on_flush: FlushCallback,
1596 on_error: Option<ErrorCallback>,
1598}
1599
1600impl WriteBehindWriter {
1601 pub fn new(backend: Arc<dyn L2CacheBackend>, on_flush: FlushCallback) -> Self {
1607 Self {
1608 backend,
1609 queue: tokio::sync::Mutex::new(Vec::new()),
1610 on_flush,
1611 on_error: None,
1612 }
1613 }
1614
1615 pub fn with_error_callback(mut self, on_error: ErrorCallback) -> Self {
1620 self.on_error = Some(on_error);
1621 self
1622 }
1623
1624 pub async fn write(
1631 &self,
1632 key: &[u8],
1633 value: &[u8],
1634 ttl: Option<Duration>,
1635 ) -> Result<(), CacheError> {
1636 let key_str = String::from_utf8_lossy(key).into_owned();
1637 self.backend.set(&key_str, value, ttl).await?;
1639 let op = WriteOp::Set {
1641 key: key_str,
1642 value: value.to_vec(),
1643 ttl,
1644 };
1645 self.queue.lock().await.push(op);
1646 Ok(())
1647 }
1648
1649 pub async fn delete(&self, key: &[u8]) -> Result<(), CacheError> {
1651 let key_str = String::from_utf8_lossy(key).into_owned();
1652 self.backend.delete(&key_str).await?;
1654 let op = WriteOp::Delete { key: key_str };
1656 self.queue.lock().await.push(op);
1657 Ok(())
1658 }
1659
1660 pub async fn flush(&self) -> Result<(), CacheError> {
1665 let ops: Vec<WriteOp> = {
1667 let mut guard = self.queue.lock().await;
1668 std::mem::take(&mut *guard)
1669 };
1670 if ops.is_empty() {
1671 return Ok(());
1672 }
1673 match (self.on_flush)(ops.clone()).await {
1675 Ok(()) => Ok(()),
1676 Err(e) => {
1677 let mut guard = self.queue.lock().await;
1679 guard.extend(ops.clone());
1680 if let Some(ref on_error) = self.on_error {
1682 on_error(ops, e.clone());
1683 }
1684 Err(e)
1685 }
1686 }
1687 }
1688
1689 pub async fn pending_count(&self) -> usize {
1691 self.queue.lock().await.len()
1692 }
1693
1694 pub fn spawn_auto_flush(self: Arc<Self>, interval: Duration) -> tokio::task::JoinHandle<()> {
1704 tokio::spawn(async move {
1705 let mut ticker = tokio::time::interval(interval);
1706 ticker.tick().await;
1708 loop {
1709 ticker.tick().await;
1710 if let Err(e) = self.flush().await {
1712 eprintln!("[WriteBehind] auto flush failed: {}", e);
1713 }
1714 }
1715 })
1716 }
1717}
1718
1719#[cfg(test)]
1724mod tests {
1725 use super::*;
1726 use crate::Value;
1727 use std::thread;
1728 use std::time::Duration;
1729
1730 #[test]
1733 fn test_cache_key_by_pk() {
1734 let key = CacheKey::by_pk("users", 1);
1735 assert_eq!(key.table, "users");
1736 assert_eq!(key.kind, CacheKeyKind::ByPk);
1737 assert_eq!(key.identifier, "1");
1738 assert_eq!(key.to_string_key(), "l2:users:pk:1");
1739 }
1740
1741 #[test]
1742 fn test_cache_key_by_query() {
1743 let key = CacheKey::by_query("orders", "abc123");
1744 assert_eq!(key.kind, CacheKeyKind::ByQuery);
1745 assert_eq!(key.to_string_key(), "l2:orders:q:abc123");
1746 }
1747
1748 #[test]
1749 fn test_cache_key_by_relation() {
1750 let key = CacheKey::by_relation("users", "posts:1");
1751 assert_eq!(key.kind, CacheKeyKind::ByRelation);
1752 assert_eq!(key.to_string_key(), "l2:users:rel:posts:1");
1753 }
1754
1755 #[test]
1756 fn test_cache_key_equality() {
1757 let k1 = CacheKey::by_pk("users", 1);
1758 let k2 = CacheKey::by_pk("users", 1);
1759 let k3 = CacheKey::by_pk("users", 2);
1760 assert_eq!(k1, k2);
1761 assert_ne!(k1, k3);
1762 }
1763
1764 #[test]
1765 fn test_cache_key_display() {
1766 let key = CacheKey::by_pk("users", 42);
1767 assert_eq!(format!("{}", key), "l2:users:pk:42");
1768 }
1769
1770 #[test]
1773 fn test_stats_hit_rate_empty() {
1774 let stats = L2CacheStats::default();
1775 assert_eq!(stats.hit_rate(), 0.0);
1776 assert_eq!(stats.total_lookups(), 0);
1777 }
1778
1779 #[test]
1780 fn test_stats_hit_rate_calculation() {
1781 let stats = L2CacheStats {
1782 hits: 80,
1783 misses: 20,
1784 ..Default::default()
1785 };
1786 assert_eq!(stats.total_lookups(), 100);
1787 assert!((stats.hit_rate() - 0.8).abs() < 0.001);
1788 assert!((stats.miss_rate() - 0.2).abs() < 0.001);
1789 }
1790
1791 #[test]
1792 fn test_stats_merge() {
1793 let mut s1 = L2CacheStats {
1794 hits: 10,
1795 misses: 5,
1796 sets: 15,
1797 evictions: 2,
1798 size: 100,
1799 };
1800 let s2 = L2CacheStats {
1801 hits: 20,
1802 misses: 10,
1803 sets: 30,
1804 evictions: 5,
1805 size: 200,
1806 };
1807 s1.merge(&s2);
1808 assert_eq!(s1.hits, 30);
1809 assert_eq!(s1.misses, 15);
1810 assert_eq!(s1.sets, 45);
1811 assert_eq!(s1.evictions, 7);
1812 assert_eq!(s1.size, 300);
1813 }
1814
1815 #[test]
1818 fn test_put_and_get() {
1819 let cache = L2Cache::new();
1820 let key = CacheKey::by_pk("users", 1);
1821
1822 cache.put(&key, Value::String("Alice".to_string()), None);
1823 let val = cache.get(&key);
1824 assert_eq!(val, Some(Value::String("Alice".to_string())));
1825 }
1826
1827 #[test]
1828 fn test_get_missing_returns_none() {
1829 let cache = L2Cache::new();
1830 let key = CacheKey::by_pk("users", 999);
1831 assert_eq!(cache.get(&key), None);
1832 }
1833
1834 #[test]
1835 fn test_overwrite_existing_key() {
1836 let cache = L2Cache::new();
1837 let key = CacheKey::by_pk("users", 1);
1838
1839 cache.put(&key, Value::String("Alice".to_string()), None);
1840 cache.put(&key, Value::String("Bob".to_string()), None);
1841 assert_eq!(cache.get(&key), Some(Value::String("Bob".to_string())));
1842 }
1843
1844 #[test]
1845 fn test_invalidate_single_key() {
1846 let cache = L2Cache::new();
1847 let key = CacheKey::by_pk("users", 1);
1848
1849 cache.put(&key, Value::I64(42), None);
1850 assert!(cache.get(&key).is_some());
1851
1852 cache.invalidate(&key);
1853 assert!(cache.get(&key).is_none());
1854 }
1855
1856 #[test]
1859 fn test_invalidate_table_removes_all_entries_for_table() {
1860 let cache = L2Cache::new();
1861
1862 let k1 = CacheKey::by_pk("users", 1);
1863 let k2 = CacheKey::by_pk("users", 2);
1864 let k3 = CacheKey::by_query("users", "hash1");
1865 let k4 = CacheKey::by_pk("orders", 1); cache.put(&k1, Value::I64(1), None);
1868 cache.put(&k2, Value::I64(2), None);
1869 cache.put(&k3, Value::I64(3), None);
1870 cache.put(&k4, Value::I64(4), None);
1871
1872 cache.invalidate_table("users");
1873
1874 assert!(cache.get(&k1).is_none());
1876 assert!(cache.get(&k2).is_none());
1877 assert!(cache.get(&k3).is_none());
1878 assert!(cache.get(&k4).is_some());
1880 }
1881
1882 #[test]
1883 fn test_invalidate_table_no_op_for_unknown_table() {
1884 let cache = L2Cache::new();
1885 let k1 = CacheKey::by_pk("users", 1);
1886 cache.put(&k1, Value::I64(1), None);
1887
1888 cache.invalidate_table("nonexistent");
1889 assert!(cache.get(&k1).is_some());
1890 }
1891
1892 #[test]
1895 fn test_ttl_expiration() {
1896 let cache = L2Cache::new();
1897 let key = CacheKey::by_pk("users", 1);
1898
1899 cache.put(&key, Value::I64(42), Some(Duration::from_millis(50)));
1900 assert!(cache.get(&key).is_some());
1901
1902 thread::sleep(Duration::from_millis(100));
1904 assert!(cache.get(&key).is_none());
1905 }
1906
1907 #[test]
1908 fn test_default_ttl_applied_when_no_explicit_ttl() {
1909 let cache = L2Cache::new().with_default_ttl(Duration::from_millis(50));
1910 let key = CacheKey::by_pk("users", 1);
1911
1912 cache.put(&key, Value::I64(42), None); assert!(cache.get(&key).is_some());
1914
1915 thread::sleep(Duration::from_millis(100));
1916 assert!(cache.get(&key).is_none());
1917 }
1918
1919 #[test]
1920 fn test_explicit_ttl_overrides_default() {
1921 let cache = L2Cache::new().with_default_ttl(Duration::from_millis(50));
1923 let key = CacheKey::by_pk("users", 1);
1924
1925 cache.put(&key, Value::I64(42), Some(Duration::MAX));
1927
1928 thread::sleep(Duration::from_millis(100));
1930 assert!(cache.get(&key).is_some());
1932 }
1933
1934 #[test]
1935 fn test_none_ttl_uses_default_ttl() {
1936 let cache = L2Cache::new().with_default_ttl(Duration::from_millis(50));
1938 let key = CacheKey::by_pk("users", 1);
1939
1940 cache.put(&key, Value::I64(42), None);
1941 assert!(cache.get(&key).is_some());
1942
1943 thread::sleep(Duration::from_millis(100));
1944 assert!(cache.get(&key).is_none());
1946 }
1947
1948 #[test]
1951 fn test_stats_tracks_hits_and_misses() {
1952 let cache = L2Cache::new();
1953
1954 let k1 = CacheKey::by_pk("users", 1);
1955 let k2 = CacheKey::by_pk("users", 2);
1956
1957 cache.put(&k1, Value::I64(1), None);
1958
1959 cache.get(&k1);
1961 cache.get(&k2);
1963 cache.get(&k2);
1964
1965 let stats = cache.stats();
1966 assert_eq!(stats.hits, 1);
1967 assert_eq!(stats.misses, 2);
1968 assert_eq!(stats.sets, 1);
1969 }
1970
1971 #[test]
1972 fn test_stats_tracks_evictions() {
1973 let cache = L2Cache::new();
1974 let k1 = CacheKey::by_pk("users", 1);
1975 let k2 = CacheKey::by_pk("users", 2);
1976
1977 cache.put(&k1, Value::I64(1), None);
1978 cache.put(&k2, Value::I64(2), None);
1979
1980 cache.invalidate(&k1); cache.invalidate_table("users"); let stats = cache.stats();
1984 assert_eq!(stats.evictions, 2);
1986 }
1987
1988 #[test]
1989 fn test_stats_reset() {
1990 let cache = L2Cache::new();
1991 let k1 = CacheKey::by_pk("users", 1);
1992
1993 cache.put(&k1, Value::I64(1), None);
1994 cache.get(&k1);
1995 cache.get(&k1);
1996
1997 let stats_before = cache.stats();
1998 assert!(stats_before.hits > 0);
1999
2000 cache.reset_stats();
2001 let stats_after = cache.stats();
2002 assert_eq!(stats_after.hits, 0);
2003 assert_eq!(stats_after.misses, 0);
2004 assert_eq!(stats_after.sets, 0);
2005 }
2006
2007 #[test]
2010 fn test_max_size_eviction() {
2011 let cache = L2Cache::new().with_max_size(3);
2012
2013 for i in 0..5 {
2014 let k = CacheKey::by_pk("users", i);
2015 cache.put(&k, Value::I64(i), None);
2016 }
2017
2018 let size = cache.size();
2020 assert_eq!(
2021 size, 3,
2022 "size should be exactly max_size after LRU eviction, got {}",
2023 size
2024 );
2025 }
2026
2027 #[test]
2028 fn test_lru_eviction_order() {
2029 let cache = L2Cache::new().with_max_size(3);
2031
2032 let k0 = CacheKey::by_pk("users", 0);
2033 let k1 = CacheKey::by_pk("users", 1);
2034 let k2 = CacheKey::by_pk("users", 2);
2035 let k3 = CacheKey::by_pk("users", 3);
2036
2037 cache.put(&k0, Value::I64(0), None);
2038 cache.put(&k1, Value::I64(1), None);
2039 cache.put(&k2, Value::I64(2), None);
2040
2041 let _ = cache.get(&k0);
2043
2044 cache.put(&k3, Value::I64(3), None);
2046
2047 assert!(
2048 cache.get(&k0).is_some(),
2049 "k0 should survive (recently accessed)"
2050 );
2051 assert!(
2052 cache.get(&k1).is_none(),
2053 "k1 should be evicted (LRU victim)"
2054 );
2055 assert!(cache.get(&k2).is_some(), "k2 should survive");
2056 assert!(
2057 cache.get(&k3).is_some(),
2058 "k3 should survive (just inserted)"
2059 );
2060 }
2061
2062 #[test]
2063 fn test_clear_all() {
2064 let cache = L2Cache::new();
2065 cache.put(&CacheKey::by_pk("users", 1), Value::I64(1), None);
2066 cache.put(&CacheKey::by_pk("users", 2), Value::I64(2), None);
2067 cache.put(&CacheKey::by_pk("orders", 1), Value::I64(3), None);
2068
2069 assert_eq!(cache.size(), 3);
2070 cache.clear();
2071 assert_eq!(cache.size(), 0);
2072 }
2073
2074 #[test]
2077 fn test_contains_does_not_update_stats() {
2078 let cache = L2Cache::new();
2079 let k1 = CacheKey::by_pk("users", 1);
2080 cache.put(&k1, Value::I64(1), None);
2081
2082 let exists = cache.contains(&k1);
2083 assert!(exists);
2084
2085 let stats = cache.stats();
2086 assert_eq!(stats.hits, 0);
2087 assert_eq!(stats.misses, 0);
2088 }
2089
2090 #[test]
2091 fn test_contains_returns_false_for_missing() {
2092 let cache = L2Cache::new();
2093 let k = CacheKey::by_pk("users", 999);
2094 assert!(!cache.contains(&k));
2095 }
2096
2097 #[test]
2098 fn test_contains_returns_false_for_expired() {
2099 let cache = L2Cache::new();
2100 let k = CacheKey::by_pk("users", 1);
2101 cache.put(&k, Value::I64(1), Some(Duration::from_millis(10)));
2102
2103 thread::sleep(Duration::from_millis(50));
2104 assert!(!cache.contains(&k));
2105 }
2106
2107 #[test]
2110 fn test_evict_expired_removes_only_expired_entries() {
2111 let cache = L2Cache::new();
2112
2113 let k1 = CacheKey::by_pk("users", 1);
2114 let k2 = CacheKey::by_pk("users", 2);
2115
2116 cache.put(&k1, Value::I64(1), Some(Duration::from_millis(10)));
2117 cache.put(&k2, Value::I64(2), None); thread::sleep(Duration::from_millis(50));
2120 let removed = cache.evict_expired();
2121
2122 assert_eq!(removed, 1);
2123 assert!(cache.get(&k1).is_none());
2124 assert!(cache.get(&k2).is_some());
2125 }
2126
2127 #[test]
2128 fn test_evict_expired_returns_zero_if_no_expired() {
2129 let cache = L2Cache::new();
2130 let k1 = CacheKey::by_pk("users", 1);
2131 cache.put(&k1, Value::I64(1), None);
2132
2133 let removed = cache.evict_expired();
2134 assert_eq!(removed, 0);
2135 }
2136
2137 #[test]
2140 fn test_concurrent_access() {
2141 let cache = std::sync::Arc::new(L2Cache::new());
2142 let mut handles = Vec::new();
2143
2144 for i in 0..4 {
2146 let c = cache.clone();
2147 handles.push(thread::spawn(move || {
2148 for j in 0..10 {
2149 let k = CacheKey::by_pk("users", i * 10 + j);
2150 c.put(&k, Value::I64(i * 10 + j), None);
2151 }
2152 }));
2153 }
2154 for h in handles {
2155 h.join().unwrap();
2156 }
2157
2158 assert_eq!(cache.size(), 40);
2159
2160 let mut handles = Vec::new();
2162 for i in 0..4 {
2163 let c = cache.clone();
2164 handles.push(thread::spawn(move || {
2165 for j in 0..10 {
2166 let k = CacheKey::by_pk("users", i * 10 + j);
2167 let v = c.get(&k);
2168 assert!(v.is_some());
2169 }
2170 }));
2171 }
2172 for h in handles {
2173 h.join().unwrap();
2174 }
2175
2176 let stats = cache.stats();
2177 assert_eq!(stats.hits, 40);
2178 }
2179
2180 #[test]
2183 fn test_default() {
2184 let cache = L2Cache::default();
2185 assert_eq!(cache.size(), 0);
2186 }
2187
2188 #[test]
2191 fn test_realistic_scenario() {
2192 let cache = L2Cache::new();
2193
2194 for i in 1..=5 {
2196 cache.put(
2197 &CacheKey::by_pk("users", i),
2198 Value::String(format!("user_{}", i)),
2199 None,
2200 );
2201 }
2202
2203 cache.put(
2205 &CacheKey::by_query("users", "active_users_hash"),
2206 Value::I64(5),
2207 None,
2208 );
2209
2210 for i in 1..=10 {
2212 let _ = cache.get(&CacheKey::by_pk("users", i));
2213 }
2214
2215 let stats = cache.stats();
2216 assert_eq!(stats.hits, 5); assert_eq!(stats.misses, 5); assert_eq!(stats.sets, 6); cache.invalidate_table("users");
2222
2223 cache.reset_stats();
2225 for i in 1..=5 {
2226 let _ = cache.get(&CacheKey::by_pk("users", i));
2227 }
2228 let stats2 = cache.stats();
2229 assert_eq!(stats2.hits, 0);
2230 assert_eq!(stats2.misses, 5);
2231 }
2232
2233 #[tokio::test]
2236 async fn test_write_behind_basic_write_and_flush() {
2237 use std::sync::atomic::{AtomicUsize, Ordering};
2238 let counter = Arc::new(AtomicUsize::new(0));
2240 let counter_clone = counter.clone();
2241 let on_flush: FlushCallback = Arc::new(move |ops: Vec<WriteOp>| {
2242 let c = counter_clone.clone();
2243 Box::pin(async move {
2244 c.fetch_add(ops.len(), Ordering::SeqCst);
2245 Ok(())
2246 })
2247 });
2248 let backend = Arc::new(InMemoryBackend::new());
2249 let writer = WriteBehindWriter::new(backend.clone(), on_flush);
2250
2251 writer.write(b"k1", b"v1", None).await.unwrap();
2253 writer.write(b"k2", b"v2", None).await.unwrap();
2254 writer.write(b"k3", b"v3", None).await.unwrap();
2255
2256 let v1 = backend.get("k1").await.unwrap();
2258 assert_eq!(v1, Some(b"v1".to_vec()));
2259
2260 assert_eq!(writer.pending_count().await, 3);
2262
2263 writer.flush().await.unwrap();
2265 assert_eq!(counter.load(Ordering::SeqCst), 3);
2266 assert_eq!(writer.pending_count().await, 0);
2267 }
2268
2269 #[tokio::test]
2270 async fn test_write_behind_delete() {
2271 let on_flush: FlushCallback =
2272 Arc::new(|_ops: Vec<WriteOp>| Box::pin(async move { Ok(()) }));
2273 let backend = Arc::new(InMemoryBackend::new());
2274 let writer = WriteBehindWriter::new(backend.clone(), on_flush);
2275
2276 writer.write(b"k1", b"v1", None).await.unwrap();
2278 assert!(backend.get("k1").await.unwrap().is_some());
2279 writer.delete(b"k1").await.unwrap();
2280 assert!(backend.get("k1").await.unwrap().is_none());
2282
2283 writer.flush().await.unwrap();
2285 assert_eq!(writer.pending_count().await, 0);
2286 }
2287
2288 #[tokio::test]
2289 async fn test_write_behind_flush_failure_retries() {
2290 let on_flush: FlushCallback = Arc::new(|_ops: Vec<WriteOp>| {
2292 Box::pin(async move { Err(CacheError::Internal("backend down".to_string())) })
2293 });
2294 let backend = Arc::new(InMemoryBackend::new());
2295 let writer = WriteBehindWriter::new(backend.clone(), on_flush);
2296
2297 writer.write(b"k1", b"v1", None).await.unwrap();
2298 let result = writer.flush().await;
2300 assert!(result.is_err());
2301 assert_eq!(writer.pending_count().await, 1);
2302 }
2303
2304 #[tokio::test]
2305 async fn test_write_behind_empty_flush_noop() {
2306 let on_flush: FlushCallback =
2307 Arc::new(|_ops: Vec<WriteOp>| Box::pin(async move { Ok(()) }));
2308 let backend = Arc::new(InMemoryBackend::new());
2309 let writer = WriteBehindWriter::new(backend, on_flush);
2310 writer.flush().await.unwrap();
2312 assert_eq!(writer.pending_count().await, 0);
2313 }
2314
2315 #[tokio::test]
2316 async fn test_write_behind_error_callback_invoked() {
2317 use std::sync::atomic::{AtomicUsize, Ordering};
2318 let error_counter = Arc::new(AtomicUsize::new(0));
2319 let ec = error_counter.clone();
2320 let on_error: ErrorCallback = Arc::new(move |_ops, _err| {
2321 ec.fetch_add(1, Ordering::SeqCst);
2322 });
2323 let on_flush: FlushCallback = Arc::new(|_ops: Vec<WriteOp>| {
2324 Box::pin(async move { Err(CacheError::Internal("fail".to_string())) })
2325 });
2326 let backend = Arc::new(InMemoryBackend::new());
2327 let writer = WriteBehindWriter::new(backend, on_flush).with_error_callback(on_error);
2328
2329 writer.write(b"k1", b"v1", None).await.unwrap();
2330 let _ = writer.flush().await;
2331 assert_eq!(error_counter.load(Ordering::SeqCst), 1);
2332 }
2333}