1use crate::cache::Cache;
50use crate::CacheError;
51use crate::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 fn table_stats(&self, table: &str) -> Option<PerTableStats> {
848 self.table_stats
849 .read()
850 .ok()
851 .and_then(|s| s.get(table).cloned())
852 }
853
854 pub fn all_table_stats(&self) -> HashMap<String, PerTableStats> {
856 self.table_stats
857 .read()
858 .map(|s| s.clone())
859 .unwrap_or_default()
860 }
861
862 pub fn contains(&self, key: &CacheKey) -> bool {
864 let key_str = key.to_string_key();
865 self.data
866 .read()
867 .map(|d| d.get(&key_str).map(|e| !e.is_expired()).unwrap_or(false))
868 .unwrap_or(false)
869 }
870
871 pub fn evict_expired(&self) -> usize {
873 let expired_keys: Vec<String> = {
874 let data = match self.data.read() {
876 Ok(d) => d,
877 Err(_) => return 0,
878 };
879 data.iter()
880 .filter(|(_, e)| e.is_expired())
881 .map(|(k, _)| k.clone())
882 .collect()
883 };
884
885 let key_to_table: HashMap<String, String> = match self.table_index.read() {
888 Ok(idx) => {
889 let mut map = HashMap::new();
890 for (table, keys) in idx.iter() {
891 for k in keys {
892 map.insert(k.clone(), table.clone());
893 }
894 }
895 map
896 }
897 Err(_) => HashMap::new(),
898 };
899
900 let mut removed = 0;
901 if !expired_keys.is_empty() {
902 let mut data = match self.data.write() {
904 Ok(d) => d,
905 Err(_) => return 0,
906 };
907 for k in &expired_keys {
908 if data.remove(k).is_some() {
909 removed += 1;
910 }
911 }
912 }
913
914 if removed > 0 {
915 if let Ok(mut order) = self.access_order.write() {
918 for k in &expired_keys {
919 order.remove(k);
920 }
921 }
922 {
923 if let Ok(mut stats) = self.stats.write() {
925 stats.evictions += removed as u64;
926 }
927 }
928 if let Ok(mut tbl_stats) = self.table_stats.write() {
930 for k in &expired_keys {
931 if let Some(table) = key_to_table.get(k) {
932 tbl_stats.entry(table.clone()).or_default().evictions += 1;
933 }
934 }
935 }
936 }
937 removed
938 }
939
940 pub fn update_ttl(&self, key: &CacheKey, ttl: Duration) -> bool {
944 let key_str = key.to_string_key();
945 let mut data = match self.data.write() {
946 Ok(d) => d,
947 Err(_) => return false,
948 };
949 if let Some(entry) = data.get_mut(&key_str) {
950 entry.expires_at = Some(Instant::now() + ttl);
951 true
952 } else {
953 false
954 }
955 }
956
957 pub fn remaining_ttl(&self, key: &CacheKey) -> Option<Option<Duration>> {
966 let key_str = key.to_string_key();
967 let data = self.data.read().ok()?;
968 let entry = data.get(&key_str)?;
969 match entry.expires_at {
970 Some(expires_at) => {
971 let now = Instant::now();
972 if expires_at <= now {
973 None
974 } else {
975 Some(Some(expires_at.duration_since(now)))
976 }
977 }
978 None => Some(None),
979 }
980 }
981}
982
983impl Cache for L2Cache {
995 fn get(&self, key: &str) -> Result<Option<Vec<u8>>, CacheError> {
996 let cache_key = CacheKey::by_pk("__cache__", key);
997 match L2Cache::get(self, &cache_key) {
998 Some(Value::Bytes(bytes)) => Ok(Some(bytes)),
999 Some(other) => {
1000 let json = serde_json::to_vec(&other)
1001 .map_err(|e| CacheError::SerializationError(e.to_string()))?;
1002 Ok(Some(json))
1003 }
1004 None => Ok(None),
1005 }
1006 }
1007
1008 fn set(&self, key: &str, value: Vec<u8>, ttl: Option<Duration>) -> Result<(), CacheError> {
1009 let cache_key = CacheKey::by_pk("__cache__", key);
1010 self.put(&cache_key, Value::Bytes(value), ttl);
1011 Ok(())
1012 }
1013
1014 fn delete(&self, key: &str) -> Result<(), CacheError> {
1015 let cache_key = CacheKey::by_pk("__cache__", key);
1016 self.invalidate(&cache_key);
1017 Ok(())
1018 }
1019
1020 fn clear(&self) -> Result<(), CacheError> {
1021 self.invalidate_table("__cache__");
1024 Ok(())
1025 }
1026
1027 fn exists(&self, key: &str) -> Result<bool, CacheError> {
1028 let cache_key = CacheKey::by_pk("__cache__", key);
1029 Ok(self.contains(&cache_key))
1030 }
1031
1032 fn expire(&self, key: &str, ttl: Duration) -> Result<(), CacheError> {
1033 let cache_key = CacheKey::by_pk("__cache__", key);
1034 if self.update_ttl(&cache_key, ttl) {
1035 Ok(())
1036 } else {
1037 Err(CacheError::NotFound(key.to_string()))
1038 }
1039 }
1040
1041 fn ttl(&self, key: &str) -> Result<Option<Duration>, CacheError> {
1042 let cache_key = CacheKey::by_pk("__cache__", key);
1043 match self.remaining_ttl(&cache_key) {
1044 None => Err(CacheError::NotFound(key.to_string())),
1045 Some(None) => Ok(None),
1046 Some(Some(d)) => Ok(Some(d)),
1047 }
1048 }
1049}
1050
1051pub type L2CacheFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, CacheError>> + Send + 'a>>;
1060
1061pub trait L2CacheBackend: Send + Sync {
1077 fn get<'a>(&'a self, key: &'a str) -> L2CacheFuture<'a, Option<Vec<u8>>>;
1079
1080 fn set<'a>(
1082 &'a self,
1083 key: &'a str,
1084 value: &'a [u8],
1085 ttl: Option<Duration>,
1086 ) -> L2CacheFuture<'a, ()>;
1087
1088 fn delete<'a>(&'a self, key: &'a str) -> L2CacheFuture<'a, ()>;
1090
1091 fn invalidate_prefix<'a>(&'a self, prefix: &'a str) -> L2CacheFuture<'a, ()>;
1093}
1094
1095pub struct InMemoryBackend {
1109 data: RwLock<InMemoryCacheData>,
1112}
1113
1114type InMemoryCacheData = HashMap<String, (Vec<u8>, Option<Instant>)>;
1116
1117impl Default for InMemoryBackend {
1118 fn default() -> Self {
1119 Self::new()
1120 }
1121}
1122
1123impl InMemoryBackend {
1124 pub fn new() -> Self {
1126 Self {
1127 data: RwLock::new(HashMap::new()),
1128 }
1129 }
1130}
1131
1132impl L2CacheBackend for InMemoryBackend {
1133 fn get<'a>(&'a self, key: &'a str) -> L2CacheFuture<'a, Option<Vec<u8>>> {
1134 let result = {
1136 let data = match self.data.read() {
1137 Ok(d) => d,
1138 Err(e) => {
1139 let err = CacheError::from(e);
1140 return Box::pin(async move { Err(err) });
1141 }
1142 };
1143 match data.get(key) {
1144 Some((value, expiry)) => {
1145 if expiry.map(|t| t <= Instant::now()).unwrap_or(false) {
1147 Ok(None)
1148 } else {
1149 Ok(Some(value.clone()))
1150 }
1151 }
1152 None => Ok(None),
1153 }
1154 };
1155 Box::pin(async move { result })
1156 }
1157
1158 fn set<'a>(
1159 &'a self,
1160 key: &'a str,
1161 value: &'a [u8],
1162 ttl: Option<Duration>,
1163 ) -> L2CacheFuture<'a, ()> {
1164 let result = {
1165 let mut data = match self.data.write() {
1166 Ok(d) => d,
1167 Err(e) => {
1168 let err = CacheError::from(e);
1169 return Box::pin(async move { Err(err) });
1170 }
1171 };
1172 let expiry = ttl.map(|d| Instant::now() + d);
1173 data.insert(key.to_string(), (value.to_vec(), expiry));
1174 Ok(())
1175 };
1176 Box::pin(async move { result })
1177 }
1178
1179 fn delete<'a>(&'a self, key: &'a str) -> L2CacheFuture<'a, ()> {
1180 let result = {
1181 let mut data = match self.data.write() {
1182 Ok(d) => d,
1183 Err(e) => {
1184 let err = CacheError::from(e);
1185 return Box::pin(async move { Err(err) });
1186 }
1187 };
1188 data.remove(key);
1189 Ok(())
1190 };
1191 Box::pin(async move { result })
1192 }
1193
1194 fn invalidate_prefix<'a>(&'a self, prefix: &'a str) -> L2CacheFuture<'a, ()> {
1195 let result = {
1196 let mut data = match self.data.write() {
1197 Ok(d) => d,
1198 Err(e) => {
1199 let err = CacheError::from(e);
1200 return Box::pin(async move { Err(err) });
1201 }
1202 };
1203 let keys_to_remove: Vec<String> = data
1205 .keys()
1206 .filter(|k| k.starts_with(prefix))
1207 .cloned()
1208 .collect();
1209 for k in keys_to_remove {
1210 data.remove(&k);
1211 }
1212 Ok(())
1213 };
1214 Box::pin(async move { result })
1215 }
1216}
1217
1218#[cfg(feature = "redis")]
1261pub struct RedisBackend {
1262 manager: redis::aio::ConnectionManager,
1264}
1265
1266#[cfg(feature = "redis")]
1267impl RedisBackend {
1268 pub async fn new(url: impl Into<String>) -> Result<Self, CacheError> {
1278 let url = url.into();
1279 let client = redis::Client::open(url.as_str())
1280 .map_err(|e| CacheError::Internal(format!("Redis client create failed: {}", e)))?;
1281 let manager = redis::aio::ConnectionManager::new(client)
1282 .await
1283 .map_err(|e| CacheError::Internal(format!("Redis connect failed: {}", e)))?;
1284 Ok(Self { manager })
1285 }
1286
1287 pub fn from_manager(manager: redis::aio::ConnectionManager) -> Self {
1289 Self { manager }
1290 }
1291
1292 async fn invalidate_prefix_inner(&self, prefix: &str) -> Result<(), CacheError> {
1306 let pattern = format!("{}*", prefix);
1307 let mut cursor: u64 = 0;
1308 loop {
1309 let mut conn = self.manager.clone();
1312 let scan_result: redis::RedisResult<(u64, Vec<String>)> = redis::cmd("SCAN")
1313 .arg(cursor)
1314 .arg("MATCH")
1315 .arg(&pattern)
1316 .arg("COUNT")
1317 .arg(100usize)
1318 .query_async(&mut conn)
1319 .await;
1320 let (next_cursor, keys): (u64, Vec<String>) = scan_result
1321 .map_err(|e| CacheError::Internal(format!("Redis SCAN failed: {}", e)))?;
1322
1323 if !keys.is_empty() {
1324 let mut pipe = redis::pipe();
1326 for k in &keys {
1327 pipe.del(k);
1328 }
1329 let del_result: redis::RedisResult<()> = pipe.query_async(&mut conn).await;
1331 del_result.map_err(|e| {
1332 CacheError::Internal(format!("Redis DEL pipeline failed: {}", e))
1333 })?;
1334 }
1335
1336 if next_cursor == 0 {
1338 break;
1339 }
1340 cursor = next_cursor;
1341 }
1342 Ok(())
1343 }
1344}
1345
1346#[cfg(feature = "redis")]
1347impl L2CacheBackend for RedisBackend {
1348 fn get<'a>(&'a self, key: &'a str) -> L2CacheFuture<'a, Option<Vec<u8>>> {
1349 Box::pin(async move {
1350 use redis::AsyncCommands;
1351 let mut conn = self.manager.clone();
1352 let value: Option<Vec<u8>> = conn
1353 .get(key)
1354 .await
1355 .map_err(|e| CacheError::Internal(format!("Redis GET failed: {}", e)))?;
1356 Ok(value)
1357 })
1358 }
1359
1360 fn set<'a>(
1361 &'a self,
1362 key: &'a str,
1363 value: &'a [u8],
1364 ttl: Option<Duration>,
1365 ) -> L2CacheFuture<'a, ()> {
1366 Box::pin(async move {
1367 use redis::AsyncCommands;
1368 let mut conn = self.manager.clone();
1369 match ttl {
1372 Some(d) => {
1373 let secs = d.as_secs();
1374 if secs > 0 {
1375 let _: () = conn.set_ex(key, value, secs).await.map_err(|e| {
1376 CacheError::Internal(format!("Redis SET EX failed: {}", e))
1377 })?;
1378 } else {
1379 let _: () = conn.set(key, value).await.map_err(|e| {
1381 CacheError::Internal(format!("Redis SET failed: {}", e))
1382 })?;
1383 let ms: i64 = d.as_millis().min(i64::MAX as u128) as i64;
1385 let _: () = conn.pexpire(key, ms).await.map_err(|e| {
1386 CacheError::Internal(format!("Redis PEXPIRE failed: {}", e))
1387 })?;
1388 }
1389 }
1390 None => {
1391 let _: () = conn
1392 .set(key, value)
1393 .await
1394 .map_err(|e| CacheError::Internal(format!("Redis SET failed: {}", e)))?;
1395 }
1396 }
1397 Ok(())
1398 })
1399 }
1400
1401 fn delete<'a>(&'a self, key: &'a str) -> L2CacheFuture<'a, ()> {
1402 Box::pin(async move {
1403 use redis::AsyncCommands;
1404 let mut conn = self.manager.clone();
1405 let _: () = conn
1406 .del(key)
1407 .await
1408 .map_err(|e| CacheError::Internal(format!("Redis DEL failed: {}", e)))?;
1409 Ok(())
1410 })
1411 }
1412
1413 fn invalidate_prefix<'a>(&'a self, prefix: &'a str) -> L2CacheFuture<'a, ()> {
1414 Box::pin(async move { self.invalidate_prefix_inner(prefix).await })
1415 }
1416}
1417
1418#[cfg(not(feature = "redis"))]
1423pub struct RedisBackend {
1424 url: String,
1426}
1427
1428#[cfg(not(feature = "redis"))]
1429impl RedisBackend {
1430 pub fn new(_url: impl Into<String>) -> Self {
1435 Self { url: _url.into() }
1436 }
1437}
1438
1439#[cfg(not(feature = "redis"))]
1440impl L2CacheBackend for RedisBackend {
1441 fn get<'a>(&'a self, _key: &'a str) -> L2CacheFuture<'a, Option<Vec<u8>>> {
1442 let url = self.url.clone();
1443 Box::pin(async move {
1444 Err(CacheError::Internal(format!(
1445 "RedisBackend not compiled: enable 'redis' feature in sz-orm-core. URL: {}",
1446 url
1447 )))
1448 })
1449 }
1450
1451 fn set<'a>(
1452 &'a self,
1453 _key: &'a str,
1454 _value: &'a [u8],
1455 _ttl: Option<Duration>,
1456 ) -> L2CacheFuture<'a, ()> {
1457 let url = self.url.clone();
1458 Box::pin(async move {
1459 Err(CacheError::Internal(format!(
1460 "RedisBackend not compiled: enable 'redis' feature in sz-orm-core. URL: {}",
1461 url
1462 )))
1463 })
1464 }
1465
1466 fn delete<'a>(&'a self, _key: &'a str) -> L2CacheFuture<'a, ()> {
1467 let url = self.url.clone();
1468 Box::pin(async move {
1469 Err(CacheError::Internal(format!(
1470 "RedisBackend not compiled: enable 'redis' feature in sz-orm-core. URL: {}",
1471 url
1472 )))
1473 })
1474 }
1475
1476 fn invalidate_prefix<'a>(&'a self, _prefix: &'a str) -> L2CacheFuture<'a, ()> {
1477 let url = self.url.clone();
1478 Box::pin(async move {
1479 Err(CacheError::Internal(format!(
1480 "RedisBackend not compiled: enable 'redis' feature in sz-orm-core. URL: {}",
1481 url
1482 )))
1483 })
1484 }
1485}
1486
1487#[derive(Debug, Clone)]
1514pub enum WriteOp {
1515 Set {
1517 key: String,
1519 value: Vec<u8>,
1521 ttl: Option<Duration>,
1523 },
1524 Delete {
1526 key: String,
1528 },
1529}
1530
1531pub type FlushCallback = Arc<
1536 dyn Fn(Vec<WriteOp>) -> Pin<Box<dyn Future<Output = Result<(), CacheError>> + Send>>
1537 + Send
1538 + Sync,
1539>;
1540
1541pub type ErrorCallback = Arc<dyn Fn(Vec<WriteOp>, CacheError) + Send + Sync>;
1543
1544pub struct WriteBehindWriter {
1583 backend: Arc<dyn L2CacheBackend>,
1585 queue: tokio::sync::Mutex<Vec<WriteOp>>,
1587 on_flush: FlushCallback,
1589 on_error: Option<ErrorCallback>,
1591}
1592
1593impl WriteBehindWriter {
1594 pub fn new(backend: Arc<dyn L2CacheBackend>, on_flush: FlushCallback) -> Self {
1600 Self {
1601 backend,
1602 queue: tokio::sync::Mutex::new(Vec::new()),
1603 on_flush,
1604 on_error: None,
1605 }
1606 }
1607
1608 pub fn with_error_callback(mut self, on_error: ErrorCallback) -> Self {
1613 self.on_error = Some(on_error);
1614 self
1615 }
1616
1617 pub async fn write(
1624 &self,
1625 key: &[u8],
1626 value: &[u8],
1627 ttl: Option<Duration>,
1628 ) -> Result<(), CacheError> {
1629 let key_str = String::from_utf8_lossy(key).into_owned();
1630 self.backend.set(&key_str, value, ttl).await?;
1632 let op = WriteOp::Set {
1634 key: key_str,
1635 value: value.to_vec(),
1636 ttl,
1637 };
1638 self.queue.lock().await.push(op);
1639 Ok(())
1640 }
1641
1642 pub async fn delete(&self, key: &[u8]) -> Result<(), CacheError> {
1644 let key_str = String::from_utf8_lossy(key).into_owned();
1645 self.backend.delete(&key_str).await?;
1647 let op = WriteOp::Delete { key: key_str };
1649 self.queue.lock().await.push(op);
1650 Ok(())
1651 }
1652
1653 pub async fn flush(&self) -> Result<(), CacheError> {
1658 let ops: Vec<WriteOp> = {
1660 let mut guard = self.queue.lock().await;
1661 std::mem::take(&mut *guard)
1662 };
1663 if ops.is_empty() {
1664 return Ok(());
1665 }
1666 match (self.on_flush)(ops.clone()).await {
1668 Ok(()) => Ok(()),
1669 Err(e) => {
1670 let mut guard = self.queue.lock().await;
1672 guard.extend(ops.clone());
1673 if let Some(ref on_error) = self.on_error {
1675 on_error(ops, e.clone());
1676 }
1677 Err(e)
1678 }
1679 }
1680 }
1681
1682 pub async fn pending_count(&self) -> usize {
1684 self.queue.lock().await.len()
1685 }
1686
1687 pub fn spawn_auto_flush(self: Arc<Self>, interval: Duration) -> tokio::task::JoinHandle<()> {
1697 tokio::spawn(async move {
1698 let mut ticker = tokio::time::interval(interval);
1699 ticker.tick().await;
1701 loop {
1702 ticker.tick().await;
1703 if let Err(e) = self.flush().await {
1705 eprintln!("[WriteBehind] auto flush failed: {}", e);
1706 }
1707 }
1708 })
1709 }
1710}
1711
1712#[cfg(test)]
1717mod tests {
1718 use super::*;
1719 use crate::Value;
1720 use std::thread;
1721 use std::time::Duration;
1722
1723 #[test]
1726 fn test_cache_key_by_pk() {
1727 let key = CacheKey::by_pk("users", 1);
1728 assert_eq!(key.table, "users");
1729 assert_eq!(key.kind, CacheKeyKind::ByPk);
1730 assert_eq!(key.identifier, "1");
1731 assert_eq!(key.to_string_key(), "l2:users:pk:1");
1732 }
1733
1734 #[test]
1735 fn test_cache_key_by_query() {
1736 let key = CacheKey::by_query("orders", "abc123");
1737 assert_eq!(key.kind, CacheKeyKind::ByQuery);
1738 assert_eq!(key.to_string_key(), "l2:orders:q:abc123");
1739 }
1740
1741 #[test]
1742 fn test_cache_key_by_relation() {
1743 let key = CacheKey::by_relation("users", "posts:1");
1744 assert_eq!(key.kind, CacheKeyKind::ByRelation);
1745 assert_eq!(key.to_string_key(), "l2:users:rel:posts:1");
1746 }
1747
1748 #[test]
1749 fn test_cache_key_equality() {
1750 let k1 = CacheKey::by_pk("users", 1);
1751 let k2 = CacheKey::by_pk("users", 1);
1752 let k3 = CacheKey::by_pk("users", 2);
1753 assert_eq!(k1, k2);
1754 assert_ne!(k1, k3);
1755 }
1756
1757 #[test]
1758 fn test_cache_key_display() {
1759 let key = CacheKey::by_pk("users", 42);
1760 assert_eq!(format!("{}", key), "l2:users:pk:42");
1761 }
1762
1763 #[test]
1766 fn test_stats_hit_rate_empty() {
1767 let stats = L2CacheStats::default();
1768 assert_eq!(stats.hit_rate(), 0.0);
1769 assert_eq!(stats.total_lookups(), 0);
1770 }
1771
1772 #[test]
1773 fn test_stats_hit_rate_calculation() {
1774 let stats = L2CacheStats {
1775 hits: 80,
1776 misses: 20,
1777 ..Default::default()
1778 };
1779 assert_eq!(stats.total_lookups(), 100);
1780 assert!((stats.hit_rate() - 0.8).abs() < 0.001);
1781 assert!((stats.miss_rate() - 0.2).abs() < 0.001);
1782 }
1783
1784 #[test]
1785 fn test_stats_merge() {
1786 let mut s1 = L2CacheStats {
1787 hits: 10,
1788 misses: 5,
1789 sets: 15,
1790 evictions: 2,
1791 size: 100,
1792 };
1793 let s2 = L2CacheStats {
1794 hits: 20,
1795 misses: 10,
1796 sets: 30,
1797 evictions: 5,
1798 size: 200,
1799 };
1800 s1.merge(&s2);
1801 assert_eq!(s1.hits, 30);
1802 assert_eq!(s1.misses, 15);
1803 assert_eq!(s1.sets, 45);
1804 assert_eq!(s1.evictions, 7);
1805 assert_eq!(s1.size, 300);
1806 }
1807
1808 #[test]
1811 fn test_put_and_get() {
1812 let cache = L2Cache::new();
1813 let key = CacheKey::by_pk("users", 1);
1814
1815 cache.put(&key, Value::String("Alice".to_string()), None);
1816 let val = cache.get(&key);
1817 assert_eq!(val, Some(Value::String("Alice".to_string())));
1818 }
1819
1820 #[test]
1821 fn test_get_missing_returns_none() {
1822 let cache = L2Cache::new();
1823 let key = CacheKey::by_pk("users", 999);
1824 assert_eq!(cache.get(&key), None);
1825 }
1826
1827 #[test]
1828 fn test_overwrite_existing_key() {
1829 let cache = L2Cache::new();
1830 let key = CacheKey::by_pk("users", 1);
1831
1832 cache.put(&key, Value::String("Alice".to_string()), None);
1833 cache.put(&key, Value::String("Bob".to_string()), None);
1834 assert_eq!(cache.get(&key), Some(Value::String("Bob".to_string())));
1835 }
1836
1837 #[test]
1838 fn test_invalidate_single_key() {
1839 let cache = L2Cache::new();
1840 let key = CacheKey::by_pk("users", 1);
1841
1842 cache.put(&key, Value::I64(42), None);
1843 assert!(cache.get(&key).is_some());
1844
1845 cache.invalidate(&key);
1846 assert!(cache.get(&key).is_none());
1847 }
1848
1849 #[test]
1852 fn test_invalidate_table_removes_all_entries_for_table() {
1853 let cache = L2Cache::new();
1854
1855 let k1 = CacheKey::by_pk("users", 1);
1856 let k2 = CacheKey::by_pk("users", 2);
1857 let k3 = CacheKey::by_query("users", "hash1");
1858 let k4 = CacheKey::by_pk("orders", 1); cache.put(&k1, Value::I64(1), None);
1861 cache.put(&k2, Value::I64(2), None);
1862 cache.put(&k3, Value::I64(3), None);
1863 cache.put(&k4, Value::I64(4), None);
1864
1865 cache.invalidate_table("users");
1866
1867 assert!(cache.get(&k1).is_none());
1869 assert!(cache.get(&k2).is_none());
1870 assert!(cache.get(&k3).is_none());
1871 assert!(cache.get(&k4).is_some());
1873 }
1874
1875 #[test]
1876 fn test_invalidate_table_no_op_for_unknown_table() {
1877 let cache = L2Cache::new();
1878 let k1 = CacheKey::by_pk("users", 1);
1879 cache.put(&k1, Value::I64(1), None);
1880
1881 cache.invalidate_table("nonexistent");
1882 assert!(cache.get(&k1).is_some());
1883 }
1884
1885 #[test]
1888 fn test_ttl_expiration() {
1889 let cache = L2Cache::new();
1890 let key = CacheKey::by_pk("users", 1);
1891
1892 cache.put(&key, Value::I64(42), Some(Duration::from_millis(50)));
1893 assert!(cache.get(&key).is_some());
1894
1895 thread::sleep(Duration::from_millis(100));
1897 assert!(cache.get(&key).is_none());
1898 }
1899
1900 #[test]
1901 fn test_default_ttl_applied_when_no_explicit_ttl() {
1902 let cache = L2Cache::new().with_default_ttl(Duration::from_millis(50));
1903 let key = CacheKey::by_pk("users", 1);
1904
1905 cache.put(&key, Value::I64(42), None); assert!(cache.get(&key).is_some());
1907
1908 thread::sleep(Duration::from_millis(100));
1909 assert!(cache.get(&key).is_none());
1910 }
1911
1912 #[test]
1913 fn test_explicit_ttl_overrides_default() {
1914 let cache = L2Cache::new().with_default_ttl(Duration::from_millis(50));
1916 let key = CacheKey::by_pk("users", 1);
1917
1918 cache.put(&key, Value::I64(42), Some(Duration::MAX));
1920
1921 thread::sleep(Duration::from_millis(100));
1923 assert!(cache.get(&key).is_some());
1925 }
1926
1927 #[test]
1928 fn test_none_ttl_uses_default_ttl() {
1929 let cache = L2Cache::new().with_default_ttl(Duration::from_millis(50));
1931 let key = CacheKey::by_pk("users", 1);
1932
1933 cache.put(&key, Value::I64(42), None);
1934 assert!(cache.get(&key).is_some());
1935
1936 thread::sleep(Duration::from_millis(100));
1937 assert!(cache.get(&key).is_none());
1939 }
1940
1941 #[test]
1944 fn test_stats_tracks_hits_and_misses() {
1945 let cache = L2Cache::new();
1946
1947 let k1 = CacheKey::by_pk("users", 1);
1948 let k2 = CacheKey::by_pk("users", 2);
1949
1950 cache.put(&k1, Value::I64(1), None);
1951
1952 cache.get(&k1);
1954 cache.get(&k2);
1956 cache.get(&k2);
1957
1958 let stats = cache.stats();
1959 assert_eq!(stats.hits, 1);
1960 assert_eq!(stats.misses, 2);
1961 assert_eq!(stats.sets, 1);
1962 }
1963
1964 #[test]
1965 fn test_stats_tracks_evictions() {
1966 let cache = L2Cache::new();
1967 let k1 = CacheKey::by_pk("users", 1);
1968 let k2 = CacheKey::by_pk("users", 2);
1969
1970 cache.put(&k1, Value::I64(1), None);
1971 cache.put(&k2, Value::I64(2), None);
1972
1973 cache.invalidate(&k1); cache.invalidate_table("users"); let stats = cache.stats();
1977 assert_eq!(stats.evictions, 2);
1979 }
1980
1981 #[test]
1982 fn test_stats_reset() {
1983 let cache = L2Cache::new();
1984 let k1 = CacheKey::by_pk("users", 1);
1985
1986 cache.put(&k1, Value::I64(1), None);
1987 cache.get(&k1);
1988 cache.get(&k1);
1989
1990 let stats_before = cache.stats();
1991 assert!(stats_before.hits > 0);
1992
1993 cache.reset_stats();
1994 let stats_after = cache.stats();
1995 assert_eq!(stats_after.hits, 0);
1996 assert_eq!(stats_after.misses, 0);
1997 assert_eq!(stats_after.sets, 0);
1998 }
1999
2000 #[test]
2003 fn test_max_size_eviction() {
2004 let cache = L2Cache::new().with_max_size(3);
2005
2006 for i in 0..5 {
2007 let k = CacheKey::by_pk("users", i);
2008 cache.put(&k, Value::I64(i), None);
2009 }
2010
2011 let size = cache.size();
2013 assert_eq!(
2014 size, 3,
2015 "size should be exactly max_size after LRU eviction, got {}",
2016 size
2017 );
2018 }
2019
2020 #[test]
2021 fn test_lru_eviction_order() {
2022 let cache = L2Cache::new().with_max_size(3);
2024
2025 let k0 = CacheKey::by_pk("users", 0);
2026 let k1 = CacheKey::by_pk("users", 1);
2027 let k2 = CacheKey::by_pk("users", 2);
2028 let k3 = CacheKey::by_pk("users", 3);
2029
2030 cache.put(&k0, Value::I64(0), None);
2031 cache.put(&k1, Value::I64(1), None);
2032 cache.put(&k2, Value::I64(2), None);
2033
2034 let _ = cache.get(&k0);
2036
2037 cache.put(&k3, Value::I64(3), None);
2039
2040 assert!(
2041 cache.get(&k0).is_some(),
2042 "k0 should survive (recently accessed)"
2043 );
2044 assert!(
2045 cache.get(&k1).is_none(),
2046 "k1 should be evicted (LRU victim)"
2047 );
2048 assert!(cache.get(&k2).is_some(), "k2 should survive");
2049 assert!(
2050 cache.get(&k3).is_some(),
2051 "k3 should survive (just inserted)"
2052 );
2053 }
2054
2055 #[test]
2056 fn test_clear_all() {
2057 let cache = L2Cache::new();
2058 cache.put(&CacheKey::by_pk("users", 1), Value::I64(1), None);
2059 cache.put(&CacheKey::by_pk("users", 2), Value::I64(2), None);
2060 cache.put(&CacheKey::by_pk("orders", 1), Value::I64(3), None);
2061
2062 assert_eq!(cache.size(), 3);
2063 cache.clear();
2064 assert_eq!(cache.size(), 0);
2065 }
2066
2067 #[test]
2070 fn test_contains_does_not_update_stats() {
2071 let cache = L2Cache::new();
2072 let k1 = CacheKey::by_pk("users", 1);
2073 cache.put(&k1, Value::I64(1), None);
2074
2075 let exists = cache.contains(&k1);
2076 assert!(exists);
2077
2078 let stats = cache.stats();
2079 assert_eq!(stats.hits, 0);
2080 assert_eq!(stats.misses, 0);
2081 }
2082
2083 #[test]
2084 fn test_contains_returns_false_for_missing() {
2085 let cache = L2Cache::new();
2086 let k = CacheKey::by_pk("users", 999);
2087 assert!(!cache.contains(&k));
2088 }
2089
2090 #[test]
2091 fn test_contains_returns_false_for_expired() {
2092 let cache = L2Cache::new();
2093 let k = CacheKey::by_pk("users", 1);
2094 cache.put(&k, Value::I64(1), Some(Duration::from_millis(10)));
2095
2096 thread::sleep(Duration::from_millis(50));
2097 assert!(!cache.contains(&k));
2098 }
2099
2100 #[test]
2103 fn test_evict_expired_removes_only_expired_entries() {
2104 let cache = L2Cache::new();
2105
2106 let k1 = CacheKey::by_pk("users", 1);
2107 let k2 = CacheKey::by_pk("users", 2);
2108
2109 cache.put(&k1, Value::I64(1), Some(Duration::from_millis(10)));
2110 cache.put(&k2, Value::I64(2), None); thread::sleep(Duration::from_millis(50));
2113 let removed = cache.evict_expired();
2114
2115 assert_eq!(removed, 1);
2116 assert!(cache.get(&k1).is_none());
2117 assert!(cache.get(&k2).is_some());
2118 }
2119
2120 #[test]
2121 fn test_evict_expired_returns_zero_if_no_expired() {
2122 let cache = L2Cache::new();
2123 let k1 = CacheKey::by_pk("users", 1);
2124 cache.put(&k1, Value::I64(1), None);
2125
2126 let removed = cache.evict_expired();
2127 assert_eq!(removed, 0);
2128 }
2129
2130 #[test]
2133 fn test_concurrent_access() {
2134 let cache = std::sync::Arc::new(L2Cache::new());
2135 let mut handles = Vec::new();
2136
2137 for i in 0..4 {
2139 let c = cache.clone();
2140 handles.push(thread::spawn(move || {
2141 for j in 0..10 {
2142 let k = CacheKey::by_pk("users", i * 10 + j);
2143 c.put(&k, Value::I64(i * 10 + j), None);
2144 }
2145 }));
2146 }
2147 for h in handles {
2148 h.join().unwrap();
2149 }
2150
2151 assert_eq!(cache.size(), 40);
2152
2153 let mut handles = Vec::new();
2155 for i in 0..4 {
2156 let c = cache.clone();
2157 handles.push(thread::spawn(move || {
2158 for j in 0..10 {
2159 let k = CacheKey::by_pk("users", i * 10 + j);
2160 let v = c.get(&k);
2161 assert!(v.is_some());
2162 }
2163 }));
2164 }
2165 for h in handles {
2166 h.join().unwrap();
2167 }
2168
2169 let stats = cache.stats();
2170 assert_eq!(stats.hits, 40);
2171 }
2172
2173 #[test]
2176 fn test_default() {
2177 let cache = L2Cache::default();
2178 assert_eq!(cache.size(), 0);
2179 }
2180
2181 #[test]
2184 fn test_realistic_scenario() {
2185 let cache = L2Cache::new();
2186
2187 for i in 1..=5 {
2189 cache.put(
2190 &CacheKey::by_pk("users", i),
2191 Value::String(format!("user_{}", i)),
2192 None,
2193 );
2194 }
2195
2196 cache.put(
2198 &CacheKey::by_query("users", "active_users_hash"),
2199 Value::I64(5),
2200 None,
2201 );
2202
2203 for i in 1..=10 {
2205 let _ = cache.get(&CacheKey::by_pk("users", i));
2206 }
2207
2208 let stats = cache.stats();
2209 assert_eq!(stats.hits, 5); assert_eq!(stats.misses, 5); assert_eq!(stats.sets, 6); cache.invalidate_table("users");
2215
2216 cache.reset_stats();
2218 for i in 1..=5 {
2219 let _ = cache.get(&CacheKey::by_pk("users", i));
2220 }
2221 let stats2 = cache.stats();
2222 assert_eq!(stats2.hits, 0);
2223 assert_eq!(stats2.misses, 5);
2224 }
2225
2226 #[tokio::test]
2229 async fn test_write_behind_basic_write_and_flush() {
2230 use std::sync::atomic::{AtomicUsize, Ordering};
2231 let counter = Arc::new(AtomicUsize::new(0));
2233 let counter_clone = counter.clone();
2234 let on_flush: FlushCallback = Arc::new(move |ops: Vec<WriteOp>| {
2235 let c = counter_clone.clone();
2236 Box::pin(async move {
2237 c.fetch_add(ops.len(), Ordering::SeqCst);
2238 Ok(())
2239 })
2240 });
2241 let backend = Arc::new(InMemoryBackend::new());
2242 let writer = WriteBehindWriter::new(backend.clone(), on_flush);
2243
2244 writer.write(b"k1", b"v1", None).await.unwrap();
2246 writer.write(b"k2", b"v2", None).await.unwrap();
2247 writer.write(b"k3", b"v3", None).await.unwrap();
2248
2249 let v1 = backend.get("k1").await.unwrap();
2251 assert_eq!(v1, Some(b"v1".to_vec()));
2252
2253 assert_eq!(writer.pending_count().await, 3);
2255
2256 writer.flush().await.unwrap();
2258 assert_eq!(counter.load(Ordering::SeqCst), 3);
2259 assert_eq!(writer.pending_count().await, 0);
2260 }
2261
2262 #[tokio::test]
2263 async fn test_write_behind_delete() {
2264 let on_flush: FlushCallback =
2265 Arc::new(|_ops: Vec<WriteOp>| Box::pin(async move { Ok(()) }));
2266 let backend = Arc::new(InMemoryBackend::new());
2267 let writer = WriteBehindWriter::new(backend.clone(), on_flush);
2268
2269 writer.write(b"k1", b"v1", None).await.unwrap();
2271 assert!(backend.get("k1").await.unwrap().is_some());
2272 writer.delete(b"k1").await.unwrap();
2273 assert!(backend.get("k1").await.unwrap().is_none());
2275
2276 writer.flush().await.unwrap();
2278 assert_eq!(writer.pending_count().await, 0);
2279 }
2280
2281 #[tokio::test]
2282 async fn test_write_behind_flush_failure_retries() {
2283 let on_flush: FlushCallback = Arc::new(|_ops: Vec<WriteOp>| {
2285 Box::pin(async move { Err(CacheError::Internal("backend down".to_string())) })
2286 });
2287 let backend = Arc::new(InMemoryBackend::new());
2288 let writer = WriteBehindWriter::new(backend.clone(), on_flush);
2289
2290 writer.write(b"k1", b"v1", None).await.unwrap();
2291 let result = writer.flush().await;
2293 assert!(result.is_err());
2294 assert_eq!(writer.pending_count().await, 1);
2295 }
2296
2297 #[tokio::test]
2298 async fn test_write_behind_empty_flush_noop() {
2299 let on_flush: FlushCallback =
2300 Arc::new(|_ops: Vec<WriteOp>| Box::pin(async move { Ok(()) }));
2301 let backend = Arc::new(InMemoryBackend::new());
2302 let writer = WriteBehindWriter::new(backend, on_flush);
2303 writer.flush().await.unwrap();
2305 assert_eq!(writer.pending_count().await, 0);
2306 }
2307
2308 #[tokio::test]
2309 async fn test_write_behind_error_callback_invoked() {
2310 use std::sync::atomic::{AtomicUsize, Ordering};
2311 let error_counter = Arc::new(AtomicUsize::new(0));
2312 let ec = error_counter.clone();
2313 let on_error: ErrorCallback = Arc::new(move |_ops, _err| {
2314 ec.fetch_add(1, Ordering::SeqCst);
2315 });
2316 let on_flush: FlushCallback = Arc::new(|_ops: Vec<WriteOp>| {
2317 Box::pin(async move { Err(CacheError::Internal("fail".to_string())) })
2318 });
2319 let backend = Arc::new(InMemoryBackend::new());
2320 let writer = WriteBehindWriter::new(backend, on_flush).with_error_callback(on_error);
2321
2322 writer.write(b"k1", b"v1", None).await.unwrap();
2323 let _ = writer.flush().await;
2324 assert_eq!(error_counter.load(Ordering::SeqCst), 1);
2325 }
2326}