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 = match self.data.write() {
596 Ok(d) => d,
597 Err(_) => return,
598 };
599 let exists = data.contains_key(&key_str);
600 if !exists && data.len() >= self.max_size {
601 let victim = {
603 match self.access_order.read() {
606 Ok(order) => {
607 let expired = order
610 .iter_keys()
611 .find(|k| data.get(*k).map(|e| e.is_expired()).unwrap_or(false))
612 .map(|s| s.to_string());
613 let lru = order.lru_key().map(|s| s.to_string());
614 expired.or(lru)
615 }
616 Err(_) => None,
617 }
618 };
619 if let Some(victim) = victim {
620 data.remove(&victim);
621 if let Ok(mut order) = self.access_order.write() {
624 order.remove(&victim);
625 }
626 }
627 }
628 data.insert(key_str.clone(), entry);
629 };
630
631 if let Ok(mut order) = self.access_order.write() {
634 order.touch(&key_str);
635 }
636
637 if let Ok(mut idx) = self.table_index.write() {
640 let keys = idx.entry(key.table.clone()).or_default();
641 if !keys.contains(&key_str) {
642 keys.push(key_str);
643 }
644 }
645
646 if let Ok(mut stats) = self.stats.write() {
649 stats.sets += 1;
650 }
651 {
653 if let Ok(mut tbl_stats) = self.table_stats.write() {
654 tbl_stats.entry(key.table.clone()).or_default().sets += 1;
655 }
656 }
657 }
658
659 pub fn get(&self, key: &CacheKey) -> Option<Value> {
663 let key_str = key.to_string_key();
664 let table_name = key.table.clone();
665 let result = {
666 let data = self.data.read().ok()?;
667 if let Some(entry) = data.get(&key_str) {
668 if entry.is_expired() {
669 None
670 } else {
671 Some(entry.value.clone())
672 }
673 } else {
674 None
675 }
676 };
677
678 if result.is_some() {
681 if let Ok(mut order) = self.access_order.write() {
682 order.touch(&key_str);
683 }
684 }
685
686 if let Ok(mut stats) = self.stats.write() {
688 if result.is_some() {
689 stats.hits += 1;
690 } else {
691 stats.misses += 1;
692 }
693 }
694 if let Ok(mut tbl_stats) = self.table_stats.write() {
696 let entry = tbl_stats.entry(table_name).or_default();
697 if result.is_some() {
698 entry.hits += 1;
699 } else {
700 entry.misses += 1;
701 }
702 }
703
704 result
705 }
706
707 pub fn invalidate(&self, key: &CacheKey) {
709 let key_str = key.to_string_key();
710 let table_name = key.table.clone();
711 let removed = {
712 let mut data = match self.data.write() {
714 Ok(d) => d,
715 Err(_) => return,
716 };
717 data.remove(&key_str).is_some()
718 };
719 if removed {
720 if let Ok(mut order) = self.access_order.write() {
722 order.remove(&key_str);
723 }
724 }
725 if removed {
726 if let Ok(mut stats) = self.stats.write() {
728 stats.evictions += 1;
729 }
730 if let Ok(mut tbl_stats) = self.table_stats.write() {
731 tbl_stats.entry(table_name).or_default().evictions += 1;
732 }
733 }
734 }
735
736 pub fn invalidate_table(&self, table: &str) {
741 let keys_to_remove: Vec<String> = {
742 let idx = match self.table_index.read() {
743 Ok(i) => i,
744 Err(_) => return,
745 };
746 idx.get(table).cloned().unwrap_or_default()
747 };
748
749 let mut actually_removed: usize = 0;
750 {
751 let mut data = match self.data.write() {
753 Ok(d) => d,
754 Err(_) => return,
755 };
756 for k in &keys_to_remove {
757 if data.remove(k).is_some() {
758 actually_removed += 1;
759 }
760 }
761 }
762
763 if actually_removed > 0 {
766 if let Ok(mut order) = self.access_order.write() {
767 for k in &keys_to_remove {
768 order.remove(k);
769 }
770 }
771 }
772
773 if let Ok(mut idx) = self.table_index.write() {
774 idx.remove(table);
775 }
776 if actually_removed > 0 {
777 if let Ok(mut stats) = self.stats.write() {
779 stats.evictions += actually_removed as u64;
780 }
781 if let Ok(mut tbl_stats) = self.table_stats.write() {
782 tbl_stats.entry(table.to_string()).or_default().evictions +=
783 actually_removed as u64;
784 }
785 }
786
787 if let Some(bus) = &self.invalidation_bus {
789 bus.publish(InvalidationMessage::InvalidateTable(table.to_string()));
790 }
791 }
792
793 pub fn clear(&self) {
795 let removed = {
796 let mut data = match self.data.write() {
798 Ok(d) => d,
799 Err(_) => return,
800 };
801 let n = data.len();
802 data.clear();
803 n
804 };
805 if let Ok(mut order) = self.access_order.write() {
806 order.clear();
807 }
808 if let Ok(mut idx) = self.table_index.write() {
809 idx.clear();
810 }
811 if let Ok(mut tbl_stats) = self.table_stats.write() {
812 tbl_stats.clear();
813 }
814 if removed > 0 {
815 if let Ok(mut stats) = self.stats.write() {
817 stats.evictions += removed as u64;
818 stats.size = 0;
819 }
820 }
821 }
822
823 pub fn size(&self) -> usize {
825 self.data.read().map(|d| d.len()).unwrap_or(0)
826 }
827
828 pub fn stats(&self) -> L2CacheStats {
830 let mut s = self.stats.read().map(|s| s.clone()).unwrap_or_default();
831 s.size = self.size();
833 s
834 }
835
836 pub fn reset_stats(&self) {
838 if let Ok(mut stats) = self.stats.write() {
839 *stats = L2CacheStats::default();
840 }
841 if let Ok(mut tbl_stats) = self.table_stats.write() {
842 tbl_stats.clear();
843 }
844 }
845
846 pub async fn get_or_load_query<F, Fut>(
879 &self,
880 table: &str,
881 sql: &str,
882 params: &[crate::value::Value],
883 ttl: Duration,
884 loader: F,
885 ) -> Result<crate::pool::QueryRows, crate::DbError>
886 where
887 F: FnOnce() -> Fut,
888 Fut: std::future::Future<Output = Result<crate::pool::QueryRows, crate::DbError>>,
889 {
890 use std::collections::hash_map::DefaultHasher;
892 use std::hash::{Hash, Hasher};
893
894 let mut hasher = DefaultHasher::new();
895 sql.hash(&mut hasher);
896 for param in params {
897 param.to_string().hash(&mut hasher);
898 }
899 let query_hash = hasher.finish();
900 let cache_key = CacheKey::by_query(table, query_hash);
901
902 if let Some(Value::Json(json_str)) = self.get(&cache_key) {
904 if let Ok(rows) = serde_json::from_str::<crate::pool::QueryRows>(&json_str) {
906 return Ok(rows);
907 }
908 }
909
910 let rows = loader().await?;
912
913 let cache_ttl = if rows.is_empty() {
915 std::cmp::max(ttl / 10, Duration::from_secs(1))
917 } else {
918 ttl
919 };
920
921 if let Ok(json_str) = serde_json::to_string(&rows) {
923 self.put(&cache_key, Value::Json(json_str), Some(cache_ttl));
924 }
925
926 Ok(rows)
927 }
928
929 pub fn invalidate_query(&self, table: &str, sql: &str, params: &[crate::value::Value]) {
933 use std::collections::hash_map::DefaultHasher;
934 use std::hash::{Hash, Hasher};
935
936 let mut hasher = DefaultHasher::new();
937 sql.hash(&mut hasher);
938 for param in params {
939 param.to_string().hash(&mut hasher);
940 }
941 let query_hash = hasher.finish();
942 let cache_key = CacheKey::by_query(table, query_hash);
943 self.invalidate(&cache_key);
944 }
945
946 pub fn table_stats(&self, table: &str) -> Option<PerTableStats> {
948 self.table_stats
949 .read()
950 .ok()
951 .and_then(|s| s.get(table).cloned())
952 }
953
954 pub fn all_table_stats(&self) -> HashMap<String, PerTableStats> {
956 self.table_stats
957 .read()
958 .map(|s| s.clone())
959 .unwrap_or_default()
960 }
961
962 pub fn contains(&self, key: &CacheKey) -> bool {
964 let key_str = key.to_string_key();
965 self.data
966 .read()
967 .map(|d| d.get(&key_str).map(|e| !e.is_expired()).unwrap_or(false))
968 .unwrap_or(false)
969 }
970
971 pub fn evict_expired(&self) -> usize {
973 let expired_keys: Vec<String> = {
974 let data = match self.data.read() {
976 Ok(d) => d,
977 Err(_) => return 0,
978 };
979 data.iter()
980 .filter(|(_, e)| e.is_expired())
981 .map(|(k, _)| k.clone())
982 .collect()
983 };
984
985 let key_to_table: HashMap<String, String> = match self.table_index.read() {
988 Ok(idx) => {
989 let mut map = HashMap::new();
990 for (table, keys) in idx.iter() {
991 for k in keys {
992 map.insert(k.clone(), table.clone());
993 }
994 }
995 map
996 }
997 Err(_) => HashMap::new(),
998 };
999
1000 let mut removed = 0;
1001 if !expired_keys.is_empty() {
1002 let mut data = match self.data.write() {
1004 Ok(d) => d,
1005 Err(_) => return 0,
1006 };
1007 for k in &expired_keys {
1008 if data.remove(k).is_some() {
1009 removed += 1;
1010 }
1011 }
1012 }
1013
1014 if removed > 0 {
1015 if let Ok(mut order) = self.access_order.write() {
1018 for k in &expired_keys {
1019 order.remove(k);
1020 }
1021 }
1022 {
1023 if let Ok(mut stats) = self.stats.write() {
1025 stats.evictions += removed as u64;
1026 }
1027 }
1028 if let Ok(mut tbl_stats) = self.table_stats.write() {
1030 for k in &expired_keys {
1031 if let Some(table) = key_to_table.get(k) {
1032 tbl_stats.entry(table.clone()).or_default().evictions += 1;
1033 }
1034 }
1035 }
1036 }
1037 removed
1038 }
1039
1040 pub fn update_ttl(&self, key: &CacheKey, ttl: Duration) -> bool {
1044 let key_str = key.to_string_key();
1045 let mut data = match self.data.write() {
1046 Ok(d) => d,
1047 Err(_) => return false,
1048 };
1049 if let Some(entry) = data.get_mut(&key_str) {
1050 entry.expires_at = Some(Instant::now() + ttl);
1051 true
1052 } else {
1053 false
1054 }
1055 }
1056
1057 pub fn remaining_ttl(&self, key: &CacheKey) -> Option<Option<Duration>> {
1066 let key_str = key.to_string_key();
1067 let data = self.data.read().ok()?;
1068 let entry = data.get(&key_str)?;
1069 match entry.expires_at {
1070 Some(expires_at) => {
1071 let now = Instant::now();
1072 if expires_at <= now {
1073 None
1074 } else {
1075 Some(Some(expires_at.duration_since(now)))
1076 }
1077 }
1078 None => Some(None),
1079 }
1080 }
1081}
1082
1083impl Cache for L2Cache {
1095 fn get(&self, key: &str) -> Result<Option<Vec<u8>>, CacheError> {
1096 let cache_key = CacheKey::by_pk("__cache__", key);
1097 match L2Cache::get(self, &cache_key) {
1098 Some(Value::Bytes(bytes)) => Ok(Some(bytes)),
1099 Some(other) => {
1100 let json = serde_json::to_vec(&other)
1101 .map_err(|e| CacheError::SerializationError(e.to_string()))?;
1102 Ok(Some(json))
1103 }
1104 None => Ok(None),
1105 }
1106 }
1107
1108 fn set(&self, key: &str, value: Vec<u8>, ttl: Option<Duration>) -> Result<(), CacheError> {
1109 let cache_key = CacheKey::by_pk("__cache__", key);
1110 self.put(&cache_key, Value::Bytes(value), ttl);
1111 Ok(())
1112 }
1113
1114 fn delete(&self, key: &str) -> Result<(), CacheError> {
1115 let cache_key = CacheKey::by_pk("__cache__", key);
1116 self.invalidate(&cache_key);
1117 Ok(())
1118 }
1119
1120 fn clear(&self) -> Result<(), CacheError> {
1121 self.invalidate_table("__cache__");
1124 Ok(())
1125 }
1126
1127 fn exists(&self, key: &str) -> Result<bool, CacheError> {
1128 let cache_key = CacheKey::by_pk("__cache__", key);
1129 Ok(self.contains(&cache_key))
1130 }
1131
1132 fn expire(&self, key: &str, ttl: Duration) -> Result<(), CacheError> {
1133 let cache_key = CacheKey::by_pk("__cache__", key);
1134 if self.update_ttl(&cache_key, ttl) {
1135 Ok(())
1136 } else {
1137 Err(CacheError::NotFound(key.to_string()))
1138 }
1139 }
1140
1141 fn ttl(&self, key: &str) -> Result<Option<Duration>, CacheError> {
1142 let cache_key = CacheKey::by_pk("__cache__", key);
1143 match self.remaining_ttl(&cache_key) {
1144 None => Err(CacheError::NotFound(key.to_string())),
1145 Some(None) => Ok(None),
1146 Some(Some(d)) => Ok(Some(d)),
1147 }
1148 }
1149}
1150
1151pub type L2CacheFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, CacheError>> + Send + 'a>>;
1160
1161pub trait L2CacheBackend: Send + Sync {
1177 fn get<'a>(&'a self, key: &'a str) -> L2CacheFuture<'a, Option<Vec<u8>>>;
1179
1180 fn set<'a>(
1182 &'a self,
1183 key: &'a str,
1184 value: &'a [u8],
1185 ttl: Option<Duration>,
1186 ) -> L2CacheFuture<'a, ()>;
1187
1188 fn delete<'a>(&'a self, key: &'a str) -> L2CacheFuture<'a, ()>;
1190
1191 fn invalidate_prefix<'a>(&'a self, prefix: &'a str) -> L2CacheFuture<'a, ()>;
1193}
1194
1195pub struct InMemoryBackend {
1209 data: RwLock<InMemoryCacheData>,
1212}
1213
1214type InMemoryCacheData = HashMap<String, (Vec<u8>, Option<Instant>)>;
1216
1217impl Default for InMemoryBackend {
1218 fn default() -> Self {
1219 Self::new()
1220 }
1221}
1222
1223impl InMemoryBackend {
1224 pub fn new() -> Self {
1226 Self {
1227 data: RwLock::new(HashMap::new()),
1228 }
1229 }
1230}
1231
1232impl L2CacheBackend for InMemoryBackend {
1233 fn get<'a>(&'a self, key: &'a str) -> L2CacheFuture<'a, Option<Vec<u8>>> {
1234 let result = {
1236 let data = match self.data.read() {
1237 Ok(d) => d,
1238 Err(e) => {
1239 let err = CacheError::from(e);
1240 return Box::pin(async move { Err(err) });
1241 }
1242 };
1243 match data.get(key) {
1244 Some((value, expiry)) => {
1245 if expiry.map(|t| t <= Instant::now()).unwrap_or(false) {
1247 Ok(None)
1248 } else {
1249 Ok(Some(value.clone()))
1250 }
1251 }
1252 None => Ok(None),
1253 }
1254 };
1255 Box::pin(async move { result })
1256 }
1257
1258 fn set<'a>(
1259 &'a self,
1260 key: &'a str,
1261 value: &'a [u8],
1262 ttl: Option<Duration>,
1263 ) -> L2CacheFuture<'a, ()> {
1264 let result = {
1265 let mut data = match self.data.write() {
1266 Ok(d) => d,
1267 Err(e) => {
1268 let err = CacheError::from(e);
1269 return Box::pin(async move { Err(err) });
1270 }
1271 };
1272 let expiry = ttl.map(|d| Instant::now() + d);
1273 data.insert(key.to_string(), (value.to_vec(), expiry));
1274 Ok(())
1275 };
1276 Box::pin(async move { result })
1277 }
1278
1279 fn delete<'a>(&'a self, key: &'a str) -> L2CacheFuture<'a, ()> {
1280 let result = {
1281 let mut data = match self.data.write() {
1282 Ok(d) => d,
1283 Err(e) => {
1284 let err = CacheError::from(e);
1285 return Box::pin(async move { Err(err) });
1286 }
1287 };
1288 data.remove(key);
1289 Ok(())
1290 };
1291 Box::pin(async move { result })
1292 }
1293
1294 fn invalidate_prefix<'a>(&'a self, prefix: &'a str) -> L2CacheFuture<'a, ()> {
1295 let result = {
1296 let mut data = match self.data.write() {
1297 Ok(d) => d,
1298 Err(e) => {
1299 let err = CacheError::from(e);
1300 return Box::pin(async move { Err(err) });
1301 }
1302 };
1303 let keys_to_remove: Vec<String> = data
1305 .keys()
1306 .filter(|k| k.starts_with(prefix))
1307 .cloned()
1308 .collect();
1309 for k in keys_to_remove {
1310 data.remove(&k);
1311 }
1312 Ok(())
1313 };
1314 Box::pin(async move { result })
1315 }
1316}
1317
1318#[cfg(feature = "redis")]
1361pub struct RedisBackend {
1362 manager: redis::aio::ConnectionManager,
1364}
1365
1366#[cfg(feature = "redis")]
1367impl RedisBackend {
1368 pub async fn new(url: impl Into<String>) -> Result<Self, CacheError> {
1378 let url = url.into();
1379 let client = redis::Client::open(url.as_str())
1380 .map_err(|e| CacheError::Internal(format!("Redis client create failed: {}", e)))?;
1381 let manager = redis::aio::ConnectionManager::new(client)
1382 .await
1383 .map_err(|e| CacheError::Internal(format!("Redis connect failed: {}", e)))?;
1384 Ok(Self { manager })
1385 }
1386
1387 pub fn from_manager(manager: redis::aio::ConnectionManager) -> Self {
1389 Self { manager }
1390 }
1391
1392 async fn invalidate_prefix_inner(&self, prefix: &str) -> Result<(), CacheError> {
1406 let pattern = format!("{}*", prefix);
1407 let mut cursor: u64 = 0;
1408 loop {
1409 let mut conn = self.manager.clone();
1412 let scan_result: redis::RedisResult<(u64, Vec<String>)> = redis::cmd("SCAN")
1413 .arg(cursor)
1414 .arg("MATCH")
1415 .arg(&pattern)
1416 .arg("COUNT")
1417 .arg(100usize)
1418 .query_async(&mut conn)
1419 .await;
1420 let (next_cursor, keys): (u64, Vec<String>) = scan_result
1421 .map_err(|e| CacheError::Internal(format!("Redis SCAN failed: {}", e)))?;
1422
1423 if !keys.is_empty() {
1424 let mut pipe = redis::pipe();
1426 for k in &keys {
1427 pipe.del(k);
1428 }
1429 let del_result: redis::RedisResult<()> = pipe.query_async(&mut conn).await;
1431 del_result.map_err(|e| {
1432 CacheError::Internal(format!("Redis DEL pipeline failed: {}", e))
1433 })?;
1434 }
1435
1436 if next_cursor == 0 {
1438 break;
1439 }
1440 cursor = next_cursor;
1441 }
1442 Ok(())
1443 }
1444}
1445
1446#[cfg(feature = "redis")]
1447impl L2CacheBackend for RedisBackend {
1448 fn get<'a>(&'a self, key: &'a str) -> L2CacheFuture<'a, Option<Vec<u8>>> {
1449 Box::pin(async move {
1450 use redis::AsyncCommands;
1451 let mut conn = self.manager.clone();
1452 let value: Option<Vec<u8>> = conn
1453 .get(key)
1454 .await
1455 .map_err(|e| CacheError::Internal(format!("Redis GET failed: {}", e)))?;
1456 Ok(value)
1457 })
1458 }
1459
1460 fn set<'a>(
1461 &'a self,
1462 key: &'a str,
1463 value: &'a [u8],
1464 ttl: Option<Duration>,
1465 ) -> L2CacheFuture<'a, ()> {
1466 Box::pin(async move {
1467 use redis::AsyncCommands;
1468 let mut conn = self.manager.clone();
1469 match ttl {
1472 Some(d) => {
1473 let secs = d.as_secs();
1474 if secs > 0 {
1475 let _: () = conn.set_ex(key, value, secs).await.map_err(|e| {
1476 CacheError::Internal(format!("Redis SET EX failed: {}", e))
1477 })?;
1478 } else {
1479 let _: () = conn.set(key, value).await.map_err(|e| {
1481 CacheError::Internal(format!("Redis SET failed: {}", e))
1482 })?;
1483 let ms: i64 = d.as_millis().min(i64::MAX as u128) as i64;
1485 let _: () = conn.pexpire(key, ms).await.map_err(|e| {
1486 CacheError::Internal(format!("Redis PEXPIRE failed: {}", e))
1487 })?;
1488 }
1489 }
1490 None => {
1491 let _: () = conn
1492 .set(key, value)
1493 .await
1494 .map_err(|e| CacheError::Internal(format!("Redis SET failed: {}", e)))?;
1495 }
1496 }
1497 Ok(())
1498 })
1499 }
1500
1501 fn delete<'a>(&'a self, key: &'a str) -> L2CacheFuture<'a, ()> {
1502 Box::pin(async move {
1503 use redis::AsyncCommands;
1504 let mut conn = self.manager.clone();
1505 let _: () = conn
1506 .del(key)
1507 .await
1508 .map_err(|e| CacheError::Internal(format!("Redis DEL failed: {}", e)))?;
1509 Ok(())
1510 })
1511 }
1512
1513 fn invalidate_prefix<'a>(&'a self, prefix: &'a str) -> L2CacheFuture<'a, ()> {
1514 Box::pin(async move { self.invalidate_prefix_inner(prefix).await })
1515 }
1516}
1517
1518#[cfg(not(feature = "redis"))]
1523pub struct RedisBackend {
1524 url: String,
1526}
1527
1528#[cfg(not(feature = "redis"))]
1529impl RedisBackend {
1530 pub fn new(_url: impl Into<String>) -> Self {
1535 Self { url: _url.into() }
1536 }
1537}
1538
1539#[cfg(not(feature = "redis"))]
1540impl L2CacheBackend for RedisBackend {
1541 fn get<'a>(&'a self, _key: &'a str) -> L2CacheFuture<'a, Option<Vec<u8>>> {
1542 let url = self.url.clone();
1543 Box::pin(async move {
1544 Err(CacheError::Internal(format!(
1545 "RedisBackend not compiled: enable 'redis' feature in sz-orm-core. URL: {}",
1546 url
1547 )))
1548 })
1549 }
1550
1551 fn set<'a>(
1552 &'a self,
1553 _key: &'a str,
1554 _value: &'a [u8],
1555 _ttl: Option<Duration>,
1556 ) -> L2CacheFuture<'a, ()> {
1557 let url = self.url.clone();
1558 Box::pin(async move {
1559 Err(CacheError::Internal(format!(
1560 "RedisBackend not compiled: enable 'redis' feature in sz-orm-core. URL: {}",
1561 url
1562 )))
1563 })
1564 }
1565
1566 fn delete<'a>(&'a self, _key: &'a str) -> L2CacheFuture<'a, ()> {
1567 let url = self.url.clone();
1568 Box::pin(async move {
1569 Err(CacheError::Internal(format!(
1570 "RedisBackend not compiled: enable 'redis' feature in sz-orm-core. URL: {}",
1571 url
1572 )))
1573 })
1574 }
1575
1576 fn invalidate_prefix<'a>(&'a self, _prefix: &'a str) -> L2CacheFuture<'a, ()> {
1577 let url = self.url.clone();
1578 Box::pin(async move {
1579 Err(CacheError::Internal(format!(
1580 "RedisBackend not compiled: enable 'redis' feature in sz-orm-core. URL: {}",
1581 url
1582 )))
1583 })
1584 }
1585}
1586
1587#[derive(Debug, Clone)]
1614pub enum WriteOp {
1615 Set {
1617 key: String,
1619 value: Vec<u8>,
1621 ttl: Option<Duration>,
1623 },
1624 Delete {
1626 key: String,
1628 },
1629}
1630
1631pub type FlushCallback = Arc<
1636 dyn Fn(Vec<WriteOp>) -> Pin<Box<dyn Future<Output = Result<(), CacheError>> + Send>>
1637 + Send
1638 + Sync,
1639>;
1640
1641pub type ErrorCallback = Arc<dyn Fn(Vec<WriteOp>, CacheError) + Send + Sync>;
1643
1644pub struct WriteBehindWriter {
1683 backend: Arc<dyn L2CacheBackend>,
1685 queue: tokio::sync::Mutex<Vec<WriteOp>>,
1687 on_flush: FlushCallback,
1689 on_error: Option<ErrorCallback>,
1691}
1692
1693impl WriteBehindWriter {
1694 pub fn new(backend: Arc<dyn L2CacheBackend>, on_flush: FlushCallback) -> Self {
1700 Self {
1701 backend,
1702 queue: tokio::sync::Mutex::new(Vec::new()),
1703 on_flush,
1704 on_error: None,
1705 }
1706 }
1707
1708 pub fn with_error_callback(mut self, on_error: ErrorCallback) -> Self {
1713 self.on_error = Some(on_error);
1714 self
1715 }
1716
1717 pub async fn write(
1724 &self,
1725 key: &[u8],
1726 value: &[u8],
1727 ttl: Option<Duration>,
1728 ) -> Result<(), CacheError> {
1729 let key_str = String::from_utf8_lossy(key).into_owned();
1730 self.backend.set(&key_str, value, ttl).await?;
1732 let op = WriteOp::Set {
1734 key: key_str,
1735 value: value.to_vec(),
1736 ttl,
1737 };
1738 self.queue.lock().await.push(op);
1739 Ok(())
1740 }
1741
1742 pub async fn delete(&self, key: &[u8]) -> Result<(), CacheError> {
1744 let key_str = String::from_utf8_lossy(key).into_owned();
1745 self.backend.delete(&key_str).await?;
1747 let op = WriteOp::Delete { key: key_str };
1749 self.queue.lock().await.push(op);
1750 Ok(())
1751 }
1752
1753 pub async fn flush(&self) -> Result<(), CacheError> {
1758 let ops: Vec<WriteOp> = {
1760 let mut guard = self.queue.lock().await;
1761 std::mem::take(&mut *guard)
1762 };
1763 if ops.is_empty() {
1764 return Ok(());
1765 }
1766 match (self.on_flush)(ops.clone()).await {
1768 Ok(()) => Ok(()),
1769 Err(e) => {
1770 let mut guard = self.queue.lock().await;
1772 guard.extend(ops.clone());
1773 if let Some(ref on_error) = self.on_error {
1775 on_error(ops, e.clone());
1776 }
1777 Err(e)
1778 }
1779 }
1780 }
1781
1782 pub async fn pending_count(&self) -> usize {
1784 self.queue.lock().await.len()
1785 }
1786
1787 pub fn spawn_auto_flush(self: Arc<Self>, interval: Duration) -> tokio::task::JoinHandle<()> {
1797 tokio::spawn(async move {
1798 let mut ticker = tokio::time::interval(interval);
1799 ticker.tick().await;
1801 loop {
1802 ticker.tick().await;
1803 if let Err(e) = self.flush().await {
1805 eprintln!("[WriteBehind] auto flush failed: {}", e);
1806 }
1807 }
1808 })
1809 }
1810}
1811
1812#[cfg(test)]
1817mod tests {
1818 use super::*;
1819 use crate::Value;
1820 use std::thread;
1821 use std::time::Duration;
1822
1823 #[test]
1826 fn test_cache_key_by_pk() {
1827 let key = CacheKey::by_pk("users", 1);
1828 assert_eq!(key.table, "users");
1829 assert_eq!(key.kind, CacheKeyKind::ByPk);
1830 assert_eq!(key.identifier, "1");
1831 assert_eq!(key.to_string_key(), "l2:users:pk:1");
1832 }
1833
1834 #[test]
1835 fn test_cache_key_by_query() {
1836 let key = CacheKey::by_query("orders", "abc123");
1837 assert_eq!(key.kind, CacheKeyKind::ByQuery);
1838 assert_eq!(key.to_string_key(), "l2:orders:q:abc123");
1839 }
1840
1841 #[test]
1842 fn test_cache_key_by_relation() {
1843 let key = CacheKey::by_relation("users", "posts:1");
1844 assert_eq!(key.kind, CacheKeyKind::ByRelation);
1845 assert_eq!(key.to_string_key(), "l2:users:rel:posts:1");
1846 }
1847
1848 #[test]
1849 fn test_cache_key_equality() {
1850 let k1 = CacheKey::by_pk("users", 1);
1851 let k2 = CacheKey::by_pk("users", 1);
1852 let k3 = CacheKey::by_pk("users", 2);
1853 assert_eq!(k1, k2);
1854 assert_ne!(k1, k3);
1855 }
1856
1857 #[test]
1858 fn test_cache_key_display() {
1859 let key = CacheKey::by_pk("users", 42);
1860 assert_eq!(format!("{}", key), "l2:users:pk:42");
1861 }
1862
1863 #[test]
1866 fn test_stats_hit_rate_empty() {
1867 let stats = L2CacheStats::default();
1868 assert_eq!(stats.hit_rate(), 0.0);
1869 assert_eq!(stats.total_lookups(), 0);
1870 }
1871
1872 #[test]
1873 fn test_stats_hit_rate_calculation() {
1874 let stats = L2CacheStats {
1875 hits: 80,
1876 misses: 20,
1877 ..Default::default()
1878 };
1879 assert_eq!(stats.total_lookups(), 100);
1880 assert!((stats.hit_rate() - 0.8).abs() < 0.001);
1881 assert!((stats.miss_rate() - 0.2).abs() < 0.001);
1882 }
1883
1884 #[test]
1885 fn test_stats_merge() {
1886 let mut s1 = L2CacheStats {
1887 hits: 10,
1888 misses: 5,
1889 sets: 15,
1890 evictions: 2,
1891 size: 100,
1892 };
1893 let s2 = L2CacheStats {
1894 hits: 20,
1895 misses: 10,
1896 sets: 30,
1897 evictions: 5,
1898 size: 200,
1899 };
1900 s1.merge(&s2);
1901 assert_eq!(s1.hits, 30);
1902 assert_eq!(s1.misses, 15);
1903 assert_eq!(s1.sets, 45);
1904 assert_eq!(s1.evictions, 7);
1905 assert_eq!(s1.size, 300);
1906 }
1907
1908 #[test]
1911 fn test_put_and_get() {
1912 let cache = L2Cache::new();
1913 let key = CacheKey::by_pk("users", 1);
1914
1915 cache.put(&key, Value::String("Alice".to_string()), None);
1916 let val = cache.get(&key);
1917 assert_eq!(val, Some(Value::String("Alice".to_string())));
1918 }
1919
1920 #[test]
1921 fn test_get_missing_returns_none() {
1922 let cache = L2Cache::new();
1923 let key = CacheKey::by_pk("users", 999);
1924 assert_eq!(cache.get(&key), None);
1925 }
1926
1927 #[test]
1928 fn test_overwrite_existing_key() {
1929 let cache = L2Cache::new();
1930 let key = CacheKey::by_pk("users", 1);
1931
1932 cache.put(&key, Value::String("Alice".to_string()), None);
1933 cache.put(&key, Value::String("Bob".to_string()), None);
1934 assert_eq!(cache.get(&key), Some(Value::String("Bob".to_string())));
1935 }
1936
1937 #[test]
1938 fn test_invalidate_single_key() {
1939 let cache = L2Cache::new();
1940 let key = CacheKey::by_pk("users", 1);
1941
1942 cache.put(&key, Value::I64(42), None);
1943 assert!(cache.get(&key).is_some());
1944
1945 cache.invalidate(&key);
1946 assert!(cache.get(&key).is_none());
1947 }
1948
1949 #[test]
1952 fn test_invalidate_table_removes_all_entries_for_table() {
1953 let cache = L2Cache::new();
1954
1955 let k1 = CacheKey::by_pk("users", 1);
1956 let k2 = CacheKey::by_pk("users", 2);
1957 let k3 = CacheKey::by_query("users", "hash1");
1958 let k4 = CacheKey::by_pk("orders", 1); cache.put(&k1, Value::I64(1), None);
1961 cache.put(&k2, Value::I64(2), None);
1962 cache.put(&k3, Value::I64(3), None);
1963 cache.put(&k4, Value::I64(4), None);
1964
1965 cache.invalidate_table("users");
1966
1967 assert!(cache.get(&k1).is_none());
1969 assert!(cache.get(&k2).is_none());
1970 assert!(cache.get(&k3).is_none());
1971 assert!(cache.get(&k4).is_some());
1973 }
1974
1975 #[test]
1976 fn test_invalidate_table_no_op_for_unknown_table() {
1977 let cache = L2Cache::new();
1978 let k1 = CacheKey::by_pk("users", 1);
1979 cache.put(&k1, Value::I64(1), None);
1980
1981 cache.invalidate_table("nonexistent");
1982 assert!(cache.get(&k1).is_some());
1983 }
1984
1985 #[test]
1988 fn test_ttl_expiration() {
1989 let cache = L2Cache::new();
1990 let key = CacheKey::by_pk("users", 1);
1991
1992 cache.put(&key, Value::I64(42), Some(Duration::from_millis(50)));
1993 assert!(cache.get(&key).is_some());
1994
1995 thread::sleep(Duration::from_millis(100));
1997 assert!(cache.get(&key).is_none());
1998 }
1999
2000 #[test]
2001 fn test_default_ttl_applied_when_no_explicit_ttl() {
2002 let cache = L2Cache::new().with_default_ttl(Duration::from_millis(50));
2003 let key = CacheKey::by_pk("users", 1);
2004
2005 cache.put(&key, Value::I64(42), None); assert!(cache.get(&key).is_some());
2007
2008 thread::sleep(Duration::from_millis(100));
2009 assert!(cache.get(&key).is_none());
2010 }
2011
2012 #[test]
2013 fn test_explicit_ttl_overrides_default() {
2014 let cache = L2Cache::new().with_default_ttl(Duration::from_millis(50));
2016 let key = CacheKey::by_pk("users", 1);
2017
2018 cache.put(&key, Value::I64(42), Some(Duration::MAX));
2020
2021 thread::sleep(Duration::from_millis(100));
2023 assert!(cache.get(&key).is_some());
2025 }
2026
2027 #[test]
2028 fn test_none_ttl_uses_default_ttl() {
2029 let cache = L2Cache::new().with_default_ttl(Duration::from_millis(50));
2031 let key = CacheKey::by_pk("users", 1);
2032
2033 cache.put(&key, Value::I64(42), None);
2034 assert!(cache.get(&key).is_some());
2035
2036 thread::sleep(Duration::from_millis(100));
2037 assert!(cache.get(&key).is_none());
2039 }
2040
2041 #[test]
2044 fn test_stats_tracks_hits_and_misses() {
2045 let cache = L2Cache::new();
2046
2047 let k1 = CacheKey::by_pk("users", 1);
2048 let k2 = CacheKey::by_pk("users", 2);
2049
2050 cache.put(&k1, Value::I64(1), None);
2051
2052 cache.get(&k1);
2054 cache.get(&k2);
2056 cache.get(&k2);
2057
2058 let stats = cache.stats();
2059 assert_eq!(stats.hits, 1);
2060 assert_eq!(stats.misses, 2);
2061 assert_eq!(stats.sets, 1);
2062 }
2063
2064 #[test]
2065 fn test_stats_tracks_evictions() {
2066 let cache = L2Cache::new();
2067 let k1 = CacheKey::by_pk("users", 1);
2068 let k2 = CacheKey::by_pk("users", 2);
2069
2070 cache.put(&k1, Value::I64(1), None);
2071 cache.put(&k2, Value::I64(2), None);
2072
2073 cache.invalidate(&k1); cache.invalidate_table("users"); let stats = cache.stats();
2077 assert_eq!(stats.evictions, 2);
2079 }
2080
2081 #[test]
2082 fn test_stats_reset() {
2083 let cache = L2Cache::new();
2084 let k1 = CacheKey::by_pk("users", 1);
2085
2086 cache.put(&k1, Value::I64(1), None);
2087 cache.get(&k1);
2088 cache.get(&k1);
2089
2090 let stats_before = cache.stats();
2091 assert!(stats_before.hits > 0);
2092
2093 cache.reset_stats();
2094 let stats_after = cache.stats();
2095 assert_eq!(stats_after.hits, 0);
2096 assert_eq!(stats_after.misses, 0);
2097 assert_eq!(stats_after.sets, 0);
2098 }
2099
2100 #[test]
2103 fn test_max_size_eviction() {
2104 let cache = L2Cache::new().with_max_size(3);
2105
2106 for i in 0..5 {
2107 let k = CacheKey::by_pk("users", i);
2108 cache.put(&k, Value::I64(i), None);
2109 }
2110
2111 let size = cache.size();
2113 assert_eq!(
2114 size, 3,
2115 "size should be exactly max_size after LRU eviction, got {}",
2116 size
2117 );
2118 }
2119
2120 #[test]
2121 fn test_lru_eviction_order() {
2122 let cache = L2Cache::new().with_max_size(3);
2124
2125 let k0 = CacheKey::by_pk("users", 0);
2126 let k1 = CacheKey::by_pk("users", 1);
2127 let k2 = CacheKey::by_pk("users", 2);
2128 let k3 = CacheKey::by_pk("users", 3);
2129
2130 cache.put(&k0, Value::I64(0), None);
2131 cache.put(&k1, Value::I64(1), None);
2132 cache.put(&k2, Value::I64(2), None);
2133
2134 let _ = cache.get(&k0);
2136
2137 cache.put(&k3, Value::I64(3), None);
2139
2140 assert!(
2141 cache.get(&k0).is_some(),
2142 "k0 should survive (recently accessed)"
2143 );
2144 assert!(
2145 cache.get(&k1).is_none(),
2146 "k1 should be evicted (LRU victim)"
2147 );
2148 assert!(cache.get(&k2).is_some(), "k2 should survive");
2149 assert!(
2150 cache.get(&k3).is_some(),
2151 "k3 should survive (just inserted)"
2152 );
2153 }
2154
2155 #[test]
2156 fn test_clear_all() {
2157 let cache = L2Cache::new();
2158 cache.put(&CacheKey::by_pk("users", 1), Value::I64(1), None);
2159 cache.put(&CacheKey::by_pk("users", 2), Value::I64(2), None);
2160 cache.put(&CacheKey::by_pk("orders", 1), Value::I64(3), None);
2161
2162 assert_eq!(cache.size(), 3);
2163 cache.clear();
2164 assert_eq!(cache.size(), 0);
2165 }
2166
2167 #[test]
2170 fn test_contains_does_not_update_stats() {
2171 let cache = L2Cache::new();
2172 let k1 = CacheKey::by_pk("users", 1);
2173 cache.put(&k1, Value::I64(1), None);
2174
2175 let exists = cache.contains(&k1);
2176 assert!(exists);
2177
2178 let stats = cache.stats();
2179 assert_eq!(stats.hits, 0);
2180 assert_eq!(stats.misses, 0);
2181 }
2182
2183 #[test]
2184 fn test_contains_returns_false_for_missing() {
2185 let cache = L2Cache::new();
2186 let k = CacheKey::by_pk("users", 999);
2187 assert!(!cache.contains(&k));
2188 }
2189
2190 #[test]
2191 fn test_contains_returns_false_for_expired() {
2192 let cache = L2Cache::new();
2193 let k = CacheKey::by_pk("users", 1);
2194 cache.put(&k, Value::I64(1), Some(Duration::from_millis(10)));
2195
2196 thread::sleep(Duration::from_millis(50));
2197 assert!(!cache.contains(&k));
2198 }
2199
2200 #[test]
2203 fn test_evict_expired_removes_only_expired_entries() {
2204 let cache = L2Cache::new();
2205
2206 let k1 = CacheKey::by_pk("users", 1);
2207 let k2 = CacheKey::by_pk("users", 2);
2208
2209 cache.put(&k1, Value::I64(1), Some(Duration::from_millis(10)));
2210 cache.put(&k2, Value::I64(2), None); thread::sleep(Duration::from_millis(50));
2213 let removed = cache.evict_expired();
2214
2215 assert_eq!(removed, 1);
2216 assert!(cache.get(&k1).is_none());
2217 assert!(cache.get(&k2).is_some());
2218 }
2219
2220 #[test]
2221 fn test_evict_expired_returns_zero_if_no_expired() {
2222 let cache = L2Cache::new();
2223 let k1 = CacheKey::by_pk("users", 1);
2224 cache.put(&k1, Value::I64(1), None);
2225
2226 let removed = cache.evict_expired();
2227 assert_eq!(removed, 0);
2228 }
2229
2230 #[test]
2233 fn test_concurrent_access() {
2234 let cache = std::sync::Arc::new(L2Cache::new());
2235 let mut handles = Vec::new();
2236
2237 for i in 0..4 {
2239 let c = cache.clone();
2240 handles.push(thread::spawn(move || {
2241 for j in 0..10 {
2242 let k = CacheKey::by_pk("users", i * 10 + j);
2243 c.put(&k, Value::I64(i * 10 + j), None);
2244 }
2245 }));
2246 }
2247 for h in handles {
2248 h.join().unwrap();
2249 }
2250
2251 assert_eq!(cache.size(), 40);
2252
2253 let mut handles = Vec::new();
2255 for i in 0..4 {
2256 let c = cache.clone();
2257 handles.push(thread::spawn(move || {
2258 for j in 0..10 {
2259 let k = CacheKey::by_pk("users", i * 10 + j);
2260 let v = c.get(&k);
2261 assert!(v.is_some());
2262 }
2263 }));
2264 }
2265 for h in handles {
2266 h.join().unwrap();
2267 }
2268
2269 let stats = cache.stats();
2270 assert_eq!(stats.hits, 40);
2271 }
2272
2273 #[test]
2276 fn test_default() {
2277 let cache = L2Cache::default();
2278 assert_eq!(cache.size(), 0);
2279 }
2280
2281 #[test]
2284 fn test_realistic_scenario() {
2285 let cache = L2Cache::new();
2286
2287 for i in 1..=5 {
2289 cache.put(
2290 &CacheKey::by_pk("users", i),
2291 Value::String(format!("user_{}", i)),
2292 None,
2293 );
2294 }
2295
2296 cache.put(
2298 &CacheKey::by_query("users", "active_users_hash"),
2299 Value::I64(5),
2300 None,
2301 );
2302
2303 for i in 1..=10 {
2305 let _ = cache.get(&CacheKey::by_pk("users", i));
2306 }
2307
2308 let stats = cache.stats();
2309 assert_eq!(stats.hits, 5); assert_eq!(stats.misses, 5); assert_eq!(stats.sets, 6); cache.invalidate_table("users");
2315
2316 cache.reset_stats();
2318 for i in 1..=5 {
2319 let _ = cache.get(&CacheKey::by_pk("users", i));
2320 }
2321 let stats2 = cache.stats();
2322 assert_eq!(stats2.hits, 0);
2323 assert_eq!(stats2.misses, 5);
2324 }
2325
2326 #[tokio::test]
2329 async fn test_write_behind_basic_write_and_flush() {
2330 use std::sync::atomic::{AtomicUsize, Ordering};
2331 let counter = Arc::new(AtomicUsize::new(0));
2333 let counter_clone = counter.clone();
2334 let on_flush: FlushCallback = Arc::new(move |ops: Vec<WriteOp>| {
2335 let c = counter_clone.clone();
2336 Box::pin(async move {
2337 c.fetch_add(ops.len(), Ordering::SeqCst);
2338 Ok(())
2339 })
2340 });
2341 let backend = Arc::new(InMemoryBackend::new());
2342 let writer = WriteBehindWriter::new(backend.clone(), on_flush);
2343
2344 writer.write(b"k1", b"v1", None).await.unwrap();
2346 writer.write(b"k2", b"v2", None).await.unwrap();
2347 writer.write(b"k3", b"v3", None).await.unwrap();
2348
2349 let v1 = backend.get("k1").await.unwrap();
2351 assert_eq!(v1, Some(b"v1".to_vec()));
2352
2353 assert_eq!(writer.pending_count().await, 3);
2355
2356 writer.flush().await.unwrap();
2358 assert_eq!(counter.load(Ordering::SeqCst), 3);
2359 assert_eq!(writer.pending_count().await, 0);
2360 }
2361
2362 #[tokio::test]
2363 async fn test_write_behind_delete() {
2364 let on_flush: FlushCallback =
2365 Arc::new(|_ops: Vec<WriteOp>| Box::pin(async move { Ok(()) }));
2366 let backend = Arc::new(InMemoryBackend::new());
2367 let writer = WriteBehindWriter::new(backend.clone(), on_flush);
2368
2369 writer.write(b"k1", b"v1", None).await.unwrap();
2371 assert!(backend.get("k1").await.unwrap().is_some());
2372 writer.delete(b"k1").await.unwrap();
2373 assert!(backend.get("k1").await.unwrap().is_none());
2375
2376 writer.flush().await.unwrap();
2378 assert_eq!(writer.pending_count().await, 0);
2379 }
2380
2381 #[tokio::test]
2382 async fn test_write_behind_flush_failure_retries() {
2383 let on_flush: FlushCallback = Arc::new(|_ops: Vec<WriteOp>| {
2385 Box::pin(async move { Err(CacheError::Internal("backend down".to_string())) })
2386 });
2387 let backend = Arc::new(InMemoryBackend::new());
2388 let writer = WriteBehindWriter::new(backend.clone(), on_flush);
2389
2390 writer.write(b"k1", b"v1", None).await.unwrap();
2391 let result = writer.flush().await;
2393 assert!(result.is_err());
2394 assert_eq!(writer.pending_count().await, 1);
2395 }
2396
2397 #[tokio::test]
2398 async fn test_write_behind_empty_flush_noop() {
2399 let on_flush: FlushCallback =
2400 Arc::new(|_ops: Vec<WriteOp>| Box::pin(async move { Ok(()) }));
2401 let backend = Arc::new(InMemoryBackend::new());
2402 let writer = WriteBehindWriter::new(backend, on_flush);
2403 writer.flush().await.unwrap();
2405 assert_eq!(writer.pending_count().await, 0);
2406 }
2407
2408 #[tokio::test]
2409 async fn test_write_behind_error_callback_invoked() {
2410 use std::sync::atomic::{AtomicUsize, Ordering};
2411 let error_counter = Arc::new(AtomicUsize::new(0));
2412 let ec = error_counter.clone();
2413 let on_error: ErrorCallback = Arc::new(move |_ops, _err| {
2414 ec.fetch_add(1, Ordering::SeqCst);
2415 });
2416 let on_flush: FlushCallback = Arc::new(|_ops: Vec<WriteOp>| {
2417 Box::pin(async move { Err(CacheError::Internal("fail".to_string())) })
2418 });
2419 let backend = Arc::new(InMemoryBackend::new());
2420 let writer = WriteBehindWriter::new(backend, on_flush).with_error_callback(on_error);
2421
2422 writer.write(b"k1", b"v1", None).await.unwrap();
2423 let _ = writer.flush().await;
2424 assert_eq!(error_counter.load(Ordering::SeqCst), 1);
2425 }
2426
2427 #[tokio::test]
2430 async fn test_query_cache_hit() {
2431 use std::collections::HashMap;
2432
2433 let cache = L2Cache::new();
2434 let mut call_count = 0;
2435
2436 let rows1 = cache
2438 .get_or_load_query(
2439 "users",
2440 "SELECT * FROM users WHERE status = ?",
2441 &[crate::value::Value::I64(1)],
2442 std::time::Duration::from_secs(300),
2443 || {
2444 call_count += 1;
2445 async {
2446 let mut row = HashMap::new();
2447 row.insert("id".to_string(), crate::value::Value::I64(1));
2448 row.insert(
2449 "name".to_string(),
2450 crate::value::Value::String("Alice".to_string()),
2451 );
2452 Ok(vec![row])
2453 }
2454 },
2455 )
2456 .await
2457 .unwrap();
2458
2459 assert_eq!(call_count, 1, "第一次查询应调用 loader");
2460 assert_eq!(rows1.len(), 1, "应返回 1 行");
2461
2462 let rows2 = cache
2464 .get_or_load_query(
2465 "users",
2466 "SELECT * FROM users WHERE status = ?",
2467 &[crate::value::Value::I64(1)],
2468 std::time::Duration::from_secs(300),
2469 || {
2470 call_count += 1;
2471 async { Ok(vec![]) }
2472 },
2473 )
2474 .await
2475 .unwrap();
2476
2477 assert_eq!(call_count, 1, "第二次查询不应调用 loader(缓存命中)");
2478 assert_eq!(rows2.len(), 1, "应返回缓存的 1 行");
2479 }
2480
2481 #[tokio::test]
2482 async fn test_query_cache_empty_result_cached() {
2483 let cache = L2Cache::new();
2484 let mut call_count = 0;
2485
2486 let rows1 = cache
2488 .get_or_load_query(
2489 "users",
2490 "SELECT * FROM users WHERE status = ?",
2491 &[crate::value::Value::I64(999)],
2492 std::time::Duration::from_secs(300),
2493 || {
2494 call_count += 1;
2495 async { Ok(vec![]) }
2496 },
2497 )
2498 .await
2499 .unwrap();
2500
2501 assert_eq!(call_count, 1, "第一次查询应调用 loader");
2502 assert_eq!(rows1.len(), 0, "应返回空结果");
2503
2504 let rows2 = cache
2506 .get_or_load_query(
2507 "users",
2508 "SELECT * FROM users WHERE status = ?",
2509 &[crate::value::Value::I64(999)],
2510 std::time::Duration::from_secs(300),
2511 || {
2512 call_count += 1;
2513 async { Ok(vec![]) }
2514 },
2515 )
2516 .await
2517 .unwrap();
2518
2519 assert_eq!(call_count, 1, "第二次查询不应调用 loader(空结果缓存命中)");
2520 assert_eq!(rows2.len(), 0, "应返回缓存的空结果");
2521 }
2522
2523 #[tokio::test]
2524 async fn test_query_cache_different_params() {
2525 use std::collections::HashMap;
2526
2527 let cache = L2Cache::new();
2528 let mut call_count = 0;
2529
2530 let _ = cache
2532 .get_or_load_query(
2533 "users",
2534 "SELECT * FROM users WHERE status = ?",
2535 &[crate::value::Value::I64(1)],
2536 std::time::Duration::from_secs(300),
2537 || {
2538 call_count += 1;
2539 async {
2540 let mut row = HashMap::new();
2541 row.insert("id".to_string(), crate::value::Value::I64(1));
2542 Ok(vec![row])
2543 }
2544 },
2545 )
2546 .await
2547 .unwrap();
2548
2549 let rows2 = cache
2551 .get_or_load_query(
2552 "users",
2553 "SELECT * FROM users WHERE status = ?",
2554 &[crate::value::Value::I64(2)],
2555 std::time::Duration::from_secs(300),
2556 || {
2557 call_count += 1;
2558 async {
2559 let mut row = HashMap::new();
2560 row.insert("id".to_string(), crate::value::Value::I64(2));
2561 row.insert(
2562 "name".to_string(),
2563 crate::value::Value::String("Bob".to_string()),
2564 );
2565 Ok(vec![row])
2566 }
2567 },
2568 )
2569 .await
2570 .unwrap();
2571
2572 assert_eq!(call_count, 2, "不同参数应调用 loader 两次");
2573 assert_eq!(rows2.len(), 1, "应返回 1 行");
2574 }
2575
2576 #[tokio::test]
2577 async fn test_query_cache_invalidate() {
2578 use std::collections::HashMap;
2579
2580 let cache = L2Cache::new();
2581 let mut call_count = 0;
2582
2583 let _ = cache
2585 .get_or_load_query(
2586 "users",
2587 "SELECT * FROM users WHERE status = ?",
2588 &[crate::value::Value::I64(1)],
2589 std::time::Duration::from_secs(300),
2590 || {
2591 call_count += 1;
2592 async {
2593 let mut row = HashMap::new();
2594 row.insert("id".to_string(), crate::value::Value::I64(1));
2595 Ok(vec![row])
2596 }
2597 },
2598 )
2599 .await
2600 .unwrap();
2601
2602 assert_eq!(call_count, 1, "第一次查询应调用 loader");
2603
2604 cache.invalidate_query(
2606 "users",
2607 "SELECT * FROM users WHERE status = ?",
2608 &[crate::value::Value::I64(1)],
2609 );
2610
2611 let _ = cache
2613 .get_or_load_query(
2614 "users",
2615 "SELECT * FROM users WHERE status = ?",
2616 &[crate::value::Value::I64(1)],
2617 std::time::Duration::from_secs(300),
2618 || {
2619 call_count += 1;
2620 async { Ok(vec![]) }
2621 },
2622 )
2623 .await
2624 .unwrap();
2625
2626 assert_eq!(call_count, 2, "失效后应重新调用 loader");
2627 }
2628
2629 #[tokio::test]
2630 async fn test_query_cache_hit_rate() {
2631 use std::collections::HashMap;
2632
2633 let cache = L2Cache::new();
2634 let mut call_count = 0;
2635
2636 for _ in 0..10 {
2638 let _ = cache
2639 .get_or_load_query(
2640 "users",
2641 "SELECT * FROM users WHERE status = ?",
2642 &[crate::value::Value::I64(1)],
2643 std::time::Duration::from_secs(300),
2644 || {
2645 call_count += 1;
2646 async {
2647 let mut row = HashMap::new();
2648 row.insert("id".to_string(), crate::value::Value::I64(1));
2649 Ok(vec![row])
2650 }
2651 },
2652 )
2653 .await
2654 .unwrap();
2655 }
2656
2657 assert_eq!(call_count, 1, "10 次查询中只有 1 次调用 loader");
2659
2660 let stats = cache.stats();
2661 assert_eq!(stats.hits, 9, "应命中 9 次");
2662 assert_eq!(stats.misses, 1, "应未命中 1 次");
2663
2664 let hit_rate = stats.hit_rate();
2665 assert!(
2666 hit_rate >= 0.8,
2667 "命中率应 >= 80%,实际: {:.2}%",
2668 hit_rate * 100.0
2669 );
2670 }
2671
2672 #[cfg(feature = "redis")]
2675 #[tokio::test]
2676 async fn test_redis_backend_invalid_url_returns_error() {
2677 let result = RedisBackend::new("not-a-valid-redis-url").await;
2679 let msg = match result {
2680 Ok(_) => panic!("无效 URL 不应连接成功"),
2681 Err(CacheError::Internal(m)) => m,
2682 Err(other) => panic!("期望 CacheError::Internal,实际: {:?}", other),
2683 };
2684 assert!(
2685 msg.contains("Redis client create failed"),
2686 "错误消息应指明 client 创建失败: {}",
2687 msg
2688 );
2689 }
2690}