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 = "prod-redis-tls")]
1368#[derive(Debug, Clone)]
1369pub struct RedisTlsConfig {
1370 pub enabled: bool,
1372 pub ca_cert_path: Option<String>,
1374 pub client_cert_path: Option<String>,
1376 pub client_key_path: Option<String>,
1378 pub sni: Option<String>,
1380 pub skip_verify: bool,
1382}
1383
1384#[cfg(feature = "prod-redis-tls")]
1385impl RedisTlsConfig {
1386 pub fn disabled() -> Self {
1388 Self {
1389 enabled: false,
1390 ca_cert_path: None,
1391 client_cert_path: None,
1392 client_key_path: None,
1393 sni: None,
1394 skip_verify: false,
1395 }
1396 }
1397
1398 pub fn enabled(ca_cert_path: impl Into<String>, sni: impl Into<String>) -> Self {
1400 Self {
1401 enabled: true,
1402 ca_cert_path: Some(ca_cert_path.into()),
1403 client_cert_path: None,
1404 client_key_path: None,
1405 sni: Some(sni.into()),
1406 skip_verify: false,
1407 }
1408 }
1409
1410 pub fn validate(&self, is_production: bool) -> Result<(), String> {
1414 if is_production && self.skip_verify {
1415 return Err("TLS skip_verify forbidden in production".to_string());
1416 }
1417 if self.enabled {
1418 if !self.skip_verify {
1419 if let Some(ref ca_path) = self.ca_cert_path {
1420 if !std::path::Path::new(ca_path).exists() {
1421 return Err(format!("CA certificate not found: {}", ca_path));
1422 }
1423 }
1424 }
1425 if let (Some(cert), Some(key)) = (&self.client_cert_path, &self.client_key_path) {
1426 if !std::path::Path::new(cert).exists() {
1427 return Err(format!("Client certificate not found: {}", cert));
1428 }
1429 if !std::path::Path::new(key).exists() {
1430 return Err(format!("Client key not found: {}", key));
1431 }
1432 }
1433 }
1434 Ok(())
1435 }
1436}
1437
1438#[cfg(feature = "prod-redis-tls")]
1442pub fn mask_redis_url(url: &str) -> String {
1443 if let Some(at_pos) = url.find('@') {
1444 if let Some(colon_pos) = url.find("://:") {
1445 let prefix = &url[..colon_pos + 4];
1446 let password = &url[colon_pos + 4..at_pos];
1447 let suffix = &url[at_pos..];
1448 if !password.is_empty() {
1449 return format!("{}***{}", prefix, suffix);
1450 }
1451 }
1452 }
1453 url.to_string()
1454}
1455
1456#[cfg(feature = "redis")]
1457impl RedisBackend {
1458 pub async fn new(url: impl Into<String>) -> Result<Self, CacheError> {
1468 let url = url.into();
1469 let client = redis::Client::open(url.as_str())
1470 .map_err(|e| CacheError::Internal(format!("Redis client create failed: {}", e)))?;
1471 let manager = redis::aio::ConnectionManager::new(client)
1472 .await
1473 .map_err(|e| CacheError::Internal(format!("Redis connect failed: {}", e)))?;
1474 Ok(Self { manager })
1475 }
1476
1477 pub fn from_manager(manager: redis::aio::ConnectionManager) -> Self {
1479 Self { manager }
1480 }
1481
1482 #[cfg(feature = "prod-redis-tls")]
1486 pub async fn new_with_tls(
1487 url: impl Into<String>,
1488 tls: &RedisTlsConfig,
1489 ) -> Result<Self, CacheError> {
1490 let url = url.into();
1491 if !tls.enabled {
1492 return Self::new(url).await;
1493 }
1494 let masked_url = mask_redis_url(&url);
1495 let client = redis::Client::open(url.as_str()).map_err(|e| {
1496 CacheError::Internal(format!(
1497 "Redis TLS client create failed: {} (url: {})",
1498 e, masked_url
1499 ))
1500 })?;
1501 let manager = redis::aio::ConnectionManager::new(client)
1502 .await
1503 .map_err(|e| {
1504 CacheError::Internal(format!(
1505 "Redis TLS handshake failed: {} (url: {})",
1506 e, masked_url
1507 ))
1508 })?;
1509 Ok(Self { manager })
1510 }
1511
1512 async fn invalidate_prefix_inner(&self, prefix: &str) -> Result<(), CacheError> {
1526 let pattern = format!("{}*", prefix);
1527 let mut cursor: u64 = 0;
1528 loop {
1529 let mut conn = self.manager.clone();
1532 let scan_result: redis::RedisResult<(u64, Vec<String>)> = redis::cmd("SCAN")
1533 .arg(cursor)
1534 .arg("MATCH")
1535 .arg(&pattern)
1536 .arg("COUNT")
1537 .arg(100usize)
1538 .query_async(&mut conn)
1539 .await;
1540 let (next_cursor, keys): (u64, Vec<String>) = scan_result
1541 .map_err(|e| CacheError::Internal(format!("Redis SCAN failed: {}", e)))?;
1542
1543 if !keys.is_empty() {
1544 let mut pipe = redis::pipe();
1546 for k in &keys {
1547 pipe.del(k);
1548 }
1549 let del_result: redis::RedisResult<()> = pipe.query_async(&mut conn).await;
1551 del_result.map_err(|e| {
1552 CacheError::Internal(format!("Redis DEL pipeline failed: {}", e))
1553 })?;
1554 }
1555
1556 if next_cursor == 0 {
1558 break;
1559 }
1560 cursor = next_cursor;
1561 }
1562 Ok(())
1563 }
1564}
1565
1566#[cfg(feature = "redis")]
1567impl L2CacheBackend for RedisBackend {
1568 fn get<'a>(&'a self, key: &'a str) -> L2CacheFuture<'a, Option<Vec<u8>>> {
1569 Box::pin(async move {
1570 use redis::AsyncCommands;
1571 let mut conn = self.manager.clone();
1572 let value: Option<Vec<u8>> = conn
1573 .get(key)
1574 .await
1575 .map_err(|e| CacheError::Internal(format!("Redis GET failed: {}", e)))?;
1576 Ok(value)
1577 })
1578 }
1579
1580 fn set<'a>(
1581 &'a self,
1582 key: &'a str,
1583 value: &'a [u8],
1584 ttl: Option<Duration>,
1585 ) -> L2CacheFuture<'a, ()> {
1586 Box::pin(async move {
1587 use redis::AsyncCommands;
1588 let mut conn = self.manager.clone();
1589 match ttl {
1592 Some(d) => {
1593 let secs = d.as_secs();
1594 if secs > 0 {
1595 let _: () = conn.set_ex(key, value, secs).await.map_err(|e| {
1596 CacheError::Internal(format!("Redis SET EX failed: {}", e))
1597 })?;
1598 } else {
1599 let _: () = conn.set(key, value).await.map_err(|e| {
1601 CacheError::Internal(format!("Redis SET failed: {}", e))
1602 })?;
1603 let ms: i64 = d.as_millis().min(i64::MAX as u128) as i64;
1605 let _: () = conn.pexpire(key, ms).await.map_err(|e| {
1606 CacheError::Internal(format!("Redis PEXPIRE failed: {}", e))
1607 })?;
1608 }
1609 }
1610 None => {
1611 let _: () = conn
1612 .set(key, value)
1613 .await
1614 .map_err(|e| CacheError::Internal(format!("Redis SET failed: {}", e)))?;
1615 }
1616 }
1617 Ok(())
1618 })
1619 }
1620
1621 fn delete<'a>(&'a self, key: &'a str) -> L2CacheFuture<'a, ()> {
1622 Box::pin(async move {
1623 use redis::AsyncCommands;
1624 let mut conn = self.manager.clone();
1625 let _: () = conn
1626 .del(key)
1627 .await
1628 .map_err(|e| CacheError::Internal(format!("Redis DEL failed: {}", e)))?;
1629 Ok(())
1630 })
1631 }
1632
1633 fn invalidate_prefix<'a>(&'a self, prefix: &'a str) -> L2CacheFuture<'a, ()> {
1634 Box::pin(async move { self.invalidate_prefix_inner(prefix).await })
1635 }
1636}
1637
1638#[cfg(not(feature = "redis"))]
1643pub struct RedisBackend {
1644 url: String,
1646}
1647
1648#[cfg(not(feature = "redis"))]
1649impl RedisBackend {
1650 pub fn new(_url: impl Into<String>) -> Self {
1655 Self { url: _url.into() }
1656 }
1657}
1658
1659#[cfg(not(feature = "redis"))]
1660impl L2CacheBackend for RedisBackend {
1661 fn get<'a>(&'a self, _key: &'a str) -> L2CacheFuture<'a, Option<Vec<u8>>> {
1662 let url = self.url.clone();
1663 Box::pin(async move {
1664 Err(CacheError::Internal(format!(
1665 "RedisBackend not compiled: enable 'redis' feature in sz-orm-core. URL: {}",
1666 url
1667 )))
1668 })
1669 }
1670
1671 fn set<'a>(
1672 &'a self,
1673 _key: &'a str,
1674 _value: &'a [u8],
1675 _ttl: Option<Duration>,
1676 ) -> L2CacheFuture<'a, ()> {
1677 let url = self.url.clone();
1678 Box::pin(async move {
1679 Err(CacheError::Internal(format!(
1680 "RedisBackend not compiled: enable 'redis' feature in sz-orm-core. URL: {}",
1681 url
1682 )))
1683 })
1684 }
1685
1686 fn delete<'a>(&'a self, _key: &'a str) -> L2CacheFuture<'a, ()> {
1687 let url = self.url.clone();
1688 Box::pin(async move {
1689 Err(CacheError::Internal(format!(
1690 "RedisBackend not compiled: enable 'redis' feature in sz-orm-core. URL: {}",
1691 url
1692 )))
1693 })
1694 }
1695
1696 fn invalidate_prefix<'a>(&'a self, _prefix: &'a str) -> L2CacheFuture<'a, ()> {
1697 let url = self.url.clone();
1698 Box::pin(async move {
1699 Err(CacheError::Internal(format!(
1700 "RedisBackend not compiled: enable 'redis' feature in sz-orm-core. URL: {}",
1701 url
1702 )))
1703 })
1704 }
1705}
1706
1707#[derive(Debug, Clone)]
1734pub enum WriteOp {
1735 Set {
1737 key: String,
1739 value: Vec<u8>,
1741 ttl: Option<Duration>,
1743 },
1744 Delete {
1746 key: String,
1748 },
1749}
1750
1751pub type FlushCallback = Arc<
1756 dyn Fn(Vec<WriteOp>) -> Pin<Box<dyn Future<Output = Result<(), CacheError>> + Send>>
1757 + Send
1758 + Sync,
1759>;
1760
1761pub type ErrorCallback = Arc<dyn Fn(Vec<WriteOp>, CacheError) + Send + Sync>;
1763
1764pub struct WriteBehindWriter {
1803 backend: Arc<dyn L2CacheBackend>,
1805 queue: tokio::sync::Mutex<Vec<WriteOp>>,
1807 on_flush: FlushCallback,
1809 on_error: Option<ErrorCallback>,
1811}
1812
1813impl WriteBehindWriter {
1814 pub fn new(backend: Arc<dyn L2CacheBackend>, on_flush: FlushCallback) -> Self {
1820 Self {
1821 backend,
1822 queue: tokio::sync::Mutex::new(Vec::new()),
1823 on_flush,
1824 on_error: None,
1825 }
1826 }
1827
1828 pub fn with_error_callback(mut self, on_error: ErrorCallback) -> Self {
1833 self.on_error = Some(on_error);
1834 self
1835 }
1836
1837 pub async fn write(
1844 &self,
1845 key: &[u8],
1846 value: &[u8],
1847 ttl: Option<Duration>,
1848 ) -> Result<(), CacheError> {
1849 let key_str = String::from_utf8_lossy(key).into_owned();
1850 self.backend.set(&key_str, value, ttl).await?;
1852 let op = WriteOp::Set {
1854 key: key_str,
1855 value: value.to_vec(),
1856 ttl,
1857 };
1858 self.queue.lock().await.push(op);
1859 Ok(())
1860 }
1861
1862 pub async fn delete(&self, key: &[u8]) -> Result<(), CacheError> {
1864 let key_str = String::from_utf8_lossy(key).into_owned();
1865 self.backend.delete(&key_str).await?;
1867 let op = WriteOp::Delete { key: key_str };
1869 self.queue.lock().await.push(op);
1870 Ok(())
1871 }
1872
1873 pub async fn flush(&self) -> Result<(), CacheError> {
1878 let ops: Vec<WriteOp> = {
1880 let mut guard = self.queue.lock().await;
1881 std::mem::take(&mut *guard)
1882 };
1883 if ops.is_empty() {
1884 return Ok(());
1885 }
1886 match (self.on_flush)(ops.clone()).await {
1888 Ok(()) => Ok(()),
1889 Err(e) => {
1890 let mut guard = self.queue.lock().await;
1892 guard.extend(ops.clone());
1893 if let Some(ref on_error) = self.on_error {
1895 on_error(ops, e.clone());
1896 }
1897 Err(e)
1898 }
1899 }
1900 }
1901
1902 pub async fn pending_count(&self) -> usize {
1904 self.queue.lock().await.len()
1905 }
1906
1907 pub fn spawn_auto_flush(self: Arc<Self>, interval: Duration) -> tokio::task::JoinHandle<()> {
1917 tokio::spawn(async move {
1918 let mut ticker = tokio::time::interval(interval);
1919 ticker.tick().await;
1921 loop {
1922 ticker.tick().await;
1923 if let Err(e) = self.flush().await {
1925 eprintln!("[WriteBehind] auto flush failed: {}", e);
1926 }
1927 }
1928 })
1929 }
1930}
1931
1932#[cfg(test)]
1937mod tests {
1938 use super::*;
1939 use crate::Value;
1940 use std::thread;
1941 use std::time::Duration;
1942
1943 #[test]
1946 fn test_cache_key_by_pk() {
1947 let key = CacheKey::by_pk("users", 1);
1948 assert_eq!(key.table, "users");
1949 assert_eq!(key.kind, CacheKeyKind::ByPk);
1950 assert_eq!(key.identifier, "1");
1951 assert_eq!(key.to_string_key(), "l2:users:pk:1");
1952 }
1953
1954 #[test]
1955 fn test_cache_key_by_query() {
1956 let key = CacheKey::by_query("orders", "abc123");
1957 assert_eq!(key.kind, CacheKeyKind::ByQuery);
1958 assert_eq!(key.to_string_key(), "l2:orders:q:abc123");
1959 }
1960
1961 #[test]
1962 fn test_cache_key_by_relation() {
1963 let key = CacheKey::by_relation("users", "posts:1");
1964 assert_eq!(key.kind, CacheKeyKind::ByRelation);
1965 assert_eq!(key.to_string_key(), "l2:users:rel:posts:1");
1966 }
1967
1968 #[test]
1969 fn test_cache_key_equality() {
1970 let k1 = CacheKey::by_pk("users", 1);
1971 let k2 = CacheKey::by_pk("users", 1);
1972 let k3 = CacheKey::by_pk("users", 2);
1973 assert_eq!(k1, k2);
1974 assert_ne!(k1, k3);
1975 }
1976
1977 #[test]
1978 fn test_cache_key_display() {
1979 let key = CacheKey::by_pk("users", 42);
1980 assert_eq!(format!("{}", key), "l2:users:pk:42");
1981 }
1982
1983 #[test]
1986 fn test_stats_hit_rate_empty() {
1987 let stats = L2CacheStats::default();
1988 assert_eq!(stats.hit_rate(), 0.0);
1989 assert_eq!(stats.total_lookups(), 0);
1990 }
1991
1992 #[test]
1993 fn test_stats_hit_rate_calculation() {
1994 let stats = L2CacheStats {
1995 hits: 80,
1996 misses: 20,
1997 ..Default::default()
1998 };
1999 assert_eq!(stats.total_lookups(), 100);
2000 assert!((stats.hit_rate() - 0.8).abs() < 0.001);
2001 assert!((stats.miss_rate() - 0.2).abs() < 0.001);
2002 }
2003
2004 #[test]
2005 fn test_stats_merge() {
2006 let mut s1 = L2CacheStats {
2007 hits: 10,
2008 misses: 5,
2009 sets: 15,
2010 evictions: 2,
2011 size: 100,
2012 };
2013 let s2 = L2CacheStats {
2014 hits: 20,
2015 misses: 10,
2016 sets: 30,
2017 evictions: 5,
2018 size: 200,
2019 };
2020 s1.merge(&s2);
2021 assert_eq!(s1.hits, 30);
2022 assert_eq!(s1.misses, 15);
2023 assert_eq!(s1.sets, 45);
2024 assert_eq!(s1.evictions, 7);
2025 assert_eq!(s1.size, 300);
2026 }
2027
2028 #[test]
2031 fn test_put_and_get() {
2032 let cache = L2Cache::new();
2033 let key = CacheKey::by_pk("users", 1);
2034
2035 cache.put(&key, Value::String("Alice".to_string()), None);
2036 let val = cache.get(&key);
2037 assert_eq!(val, Some(Value::String("Alice".to_string())));
2038 }
2039
2040 #[test]
2041 fn test_get_missing_returns_none() {
2042 let cache = L2Cache::new();
2043 let key = CacheKey::by_pk("users", 999);
2044 assert_eq!(cache.get(&key), None);
2045 }
2046
2047 #[test]
2048 fn test_overwrite_existing_key() {
2049 let cache = L2Cache::new();
2050 let key = CacheKey::by_pk("users", 1);
2051
2052 cache.put(&key, Value::String("Alice".to_string()), None);
2053 cache.put(&key, Value::String("Bob".to_string()), None);
2054 assert_eq!(cache.get(&key), Some(Value::String("Bob".to_string())));
2055 }
2056
2057 #[test]
2058 fn test_invalidate_single_key() {
2059 let cache = L2Cache::new();
2060 let key = CacheKey::by_pk("users", 1);
2061
2062 cache.put(&key, Value::I64(42), None);
2063 assert!(cache.get(&key).is_some());
2064
2065 cache.invalidate(&key);
2066 assert!(cache.get(&key).is_none());
2067 }
2068
2069 #[test]
2072 fn test_invalidate_table_removes_all_entries_for_table() {
2073 let cache = L2Cache::new();
2074
2075 let k1 = CacheKey::by_pk("users", 1);
2076 let k2 = CacheKey::by_pk("users", 2);
2077 let k3 = CacheKey::by_query("users", "hash1");
2078 let k4 = CacheKey::by_pk("orders", 1); cache.put(&k1, Value::I64(1), None);
2081 cache.put(&k2, Value::I64(2), None);
2082 cache.put(&k3, Value::I64(3), None);
2083 cache.put(&k4, Value::I64(4), None);
2084
2085 cache.invalidate_table("users");
2086
2087 assert!(cache.get(&k1).is_none());
2089 assert!(cache.get(&k2).is_none());
2090 assert!(cache.get(&k3).is_none());
2091 assert!(cache.get(&k4).is_some());
2093 }
2094
2095 #[test]
2096 fn test_invalidate_table_no_op_for_unknown_table() {
2097 let cache = L2Cache::new();
2098 let k1 = CacheKey::by_pk("users", 1);
2099 cache.put(&k1, Value::I64(1), None);
2100
2101 cache.invalidate_table("nonexistent");
2102 assert!(cache.get(&k1).is_some());
2103 }
2104
2105 #[test]
2108 fn test_ttl_expiration() {
2109 let cache = L2Cache::new();
2110 let key = CacheKey::by_pk("users", 1);
2111
2112 cache.put(&key, Value::I64(42), Some(Duration::from_millis(50)));
2113 assert!(cache.get(&key).is_some());
2114
2115 thread::sleep(Duration::from_millis(100));
2117 assert!(cache.get(&key).is_none());
2118 }
2119
2120 #[test]
2121 fn test_default_ttl_applied_when_no_explicit_ttl() {
2122 let cache = L2Cache::new().with_default_ttl(Duration::from_millis(50));
2123 let key = CacheKey::by_pk("users", 1);
2124
2125 cache.put(&key, Value::I64(42), None); assert!(cache.get(&key).is_some());
2127
2128 thread::sleep(Duration::from_millis(100));
2129 assert!(cache.get(&key).is_none());
2130 }
2131
2132 #[test]
2133 fn test_explicit_ttl_overrides_default() {
2134 let cache = L2Cache::new().with_default_ttl(Duration::from_millis(50));
2136 let key = CacheKey::by_pk("users", 1);
2137
2138 cache.put(&key, Value::I64(42), Some(Duration::MAX));
2140
2141 thread::sleep(Duration::from_millis(100));
2143 assert!(cache.get(&key).is_some());
2145 }
2146
2147 #[test]
2148 fn test_none_ttl_uses_default_ttl() {
2149 let cache = L2Cache::new().with_default_ttl(Duration::from_millis(50));
2151 let key = CacheKey::by_pk("users", 1);
2152
2153 cache.put(&key, Value::I64(42), None);
2154 assert!(cache.get(&key).is_some());
2155
2156 thread::sleep(Duration::from_millis(100));
2157 assert!(cache.get(&key).is_none());
2159 }
2160
2161 #[test]
2164 fn test_stats_tracks_hits_and_misses() {
2165 let cache = L2Cache::new();
2166
2167 let k1 = CacheKey::by_pk("users", 1);
2168 let k2 = CacheKey::by_pk("users", 2);
2169
2170 cache.put(&k1, Value::I64(1), None);
2171
2172 cache.get(&k1);
2174 cache.get(&k2);
2176 cache.get(&k2);
2177
2178 let stats = cache.stats();
2179 assert_eq!(stats.hits, 1);
2180 assert_eq!(stats.misses, 2);
2181 assert_eq!(stats.sets, 1);
2182 }
2183
2184 #[test]
2185 fn test_stats_tracks_evictions() {
2186 let cache = L2Cache::new();
2187 let k1 = CacheKey::by_pk("users", 1);
2188 let k2 = CacheKey::by_pk("users", 2);
2189
2190 cache.put(&k1, Value::I64(1), None);
2191 cache.put(&k2, Value::I64(2), None);
2192
2193 cache.invalidate(&k1); cache.invalidate_table("users"); let stats = cache.stats();
2197 assert_eq!(stats.evictions, 2);
2199 }
2200
2201 #[test]
2202 fn test_stats_reset() {
2203 let cache = L2Cache::new();
2204 let k1 = CacheKey::by_pk("users", 1);
2205
2206 cache.put(&k1, Value::I64(1), None);
2207 cache.get(&k1);
2208 cache.get(&k1);
2209
2210 let stats_before = cache.stats();
2211 assert!(stats_before.hits > 0);
2212
2213 cache.reset_stats();
2214 let stats_after = cache.stats();
2215 assert_eq!(stats_after.hits, 0);
2216 assert_eq!(stats_after.misses, 0);
2217 assert_eq!(stats_after.sets, 0);
2218 }
2219
2220 #[test]
2223 fn test_max_size_eviction() {
2224 let cache = L2Cache::new().with_max_size(3);
2225
2226 for i in 0..5 {
2227 let k = CacheKey::by_pk("users", i);
2228 cache.put(&k, Value::I64(i), None);
2229 }
2230
2231 let size = cache.size();
2233 assert_eq!(
2234 size, 3,
2235 "size should be exactly max_size after LRU eviction, got {}",
2236 size
2237 );
2238 }
2239
2240 #[test]
2241 fn test_lru_eviction_order() {
2242 let cache = L2Cache::new().with_max_size(3);
2244
2245 let k0 = CacheKey::by_pk("users", 0);
2246 let k1 = CacheKey::by_pk("users", 1);
2247 let k2 = CacheKey::by_pk("users", 2);
2248 let k3 = CacheKey::by_pk("users", 3);
2249
2250 cache.put(&k0, Value::I64(0), None);
2251 cache.put(&k1, Value::I64(1), None);
2252 cache.put(&k2, Value::I64(2), None);
2253
2254 let _ = cache.get(&k0);
2256
2257 cache.put(&k3, Value::I64(3), None);
2259
2260 assert!(
2261 cache.get(&k0).is_some(),
2262 "k0 should survive (recently accessed)"
2263 );
2264 assert!(
2265 cache.get(&k1).is_none(),
2266 "k1 should be evicted (LRU victim)"
2267 );
2268 assert!(cache.get(&k2).is_some(), "k2 should survive");
2269 assert!(
2270 cache.get(&k3).is_some(),
2271 "k3 should survive (just inserted)"
2272 );
2273 }
2274
2275 #[test]
2276 fn test_clear_all() {
2277 let cache = L2Cache::new();
2278 cache.put(&CacheKey::by_pk("users", 1), Value::I64(1), None);
2279 cache.put(&CacheKey::by_pk("users", 2), Value::I64(2), None);
2280 cache.put(&CacheKey::by_pk("orders", 1), Value::I64(3), None);
2281
2282 assert_eq!(cache.size(), 3);
2283 cache.clear();
2284 assert_eq!(cache.size(), 0);
2285 }
2286
2287 #[test]
2290 fn test_contains_does_not_update_stats() {
2291 let cache = L2Cache::new();
2292 let k1 = CacheKey::by_pk("users", 1);
2293 cache.put(&k1, Value::I64(1), None);
2294
2295 let exists = cache.contains(&k1);
2296 assert!(exists);
2297
2298 let stats = cache.stats();
2299 assert_eq!(stats.hits, 0);
2300 assert_eq!(stats.misses, 0);
2301 }
2302
2303 #[test]
2304 fn test_contains_returns_false_for_missing() {
2305 let cache = L2Cache::new();
2306 let k = CacheKey::by_pk("users", 999);
2307 assert!(!cache.contains(&k));
2308 }
2309
2310 #[test]
2311 fn test_contains_returns_false_for_expired() {
2312 let cache = L2Cache::new();
2313 let k = CacheKey::by_pk("users", 1);
2314 cache.put(&k, Value::I64(1), Some(Duration::from_millis(10)));
2315
2316 thread::sleep(Duration::from_millis(50));
2317 assert!(!cache.contains(&k));
2318 }
2319
2320 #[test]
2323 fn test_evict_expired_removes_only_expired_entries() {
2324 let cache = L2Cache::new();
2325
2326 let k1 = CacheKey::by_pk("users", 1);
2327 let k2 = CacheKey::by_pk("users", 2);
2328
2329 cache.put(&k1, Value::I64(1), Some(Duration::from_millis(10)));
2330 cache.put(&k2, Value::I64(2), None); thread::sleep(Duration::from_millis(50));
2333 let removed = cache.evict_expired();
2334
2335 assert_eq!(removed, 1);
2336 assert!(cache.get(&k1).is_none());
2337 assert!(cache.get(&k2).is_some());
2338 }
2339
2340 #[test]
2341 fn test_evict_expired_returns_zero_if_no_expired() {
2342 let cache = L2Cache::new();
2343 let k1 = CacheKey::by_pk("users", 1);
2344 cache.put(&k1, Value::I64(1), None);
2345
2346 let removed = cache.evict_expired();
2347 assert_eq!(removed, 0);
2348 }
2349
2350 #[test]
2353 fn test_concurrent_access() {
2354 let cache = std::sync::Arc::new(L2Cache::new());
2355 let mut handles = Vec::new();
2356
2357 for i in 0..4 {
2359 let c = cache.clone();
2360 handles.push(thread::spawn(move || {
2361 for j in 0..10 {
2362 let k = CacheKey::by_pk("users", i * 10 + j);
2363 c.put(&k, Value::I64(i * 10 + j), None);
2364 }
2365 }));
2366 }
2367 for h in handles {
2368 h.join().unwrap();
2369 }
2370
2371 assert_eq!(cache.size(), 40);
2372
2373 let mut handles = Vec::new();
2375 for i in 0..4 {
2376 let c = cache.clone();
2377 handles.push(thread::spawn(move || {
2378 for j in 0..10 {
2379 let k = CacheKey::by_pk("users", i * 10 + j);
2380 let v = c.get(&k);
2381 assert!(v.is_some());
2382 }
2383 }));
2384 }
2385 for h in handles {
2386 h.join().unwrap();
2387 }
2388
2389 let stats = cache.stats();
2390 assert_eq!(stats.hits, 40);
2391 }
2392
2393 #[test]
2396 fn test_default() {
2397 let cache = L2Cache::default();
2398 assert_eq!(cache.size(), 0);
2399 }
2400
2401 #[test]
2404 fn test_realistic_scenario() {
2405 let cache = L2Cache::new();
2406
2407 for i in 1..=5 {
2409 cache.put(
2410 &CacheKey::by_pk("users", i),
2411 Value::String(format!("user_{}", i)),
2412 None,
2413 );
2414 }
2415
2416 cache.put(
2418 &CacheKey::by_query("users", "active_users_hash"),
2419 Value::I64(5),
2420 None,
2421 );
2422
2423 for i in 1..=10 {
2425 let _ = cache.get(&CacheKey::by_pk("users", i));
2426 }
2427
2428 let stats = cache.stats();
2429 assert_eq!(stats.hits, 5); assert_eq!(stats.misses, 5); assert_eq!(stats.sets, 6); cache.invalidate_table("users");
2435
2436 cache.reset_stats();
2438 for i in 1..=5 {
2439 let _ = cache.get(&CacheKey::by_pk("users", i));
2440 }
2441 let stats2 = cache.stats();
2442 assert_eq!(stats2.hits, 0);
2443 assert_eq!(stats2.misses, 5);
2444 }
2445
2446 #[tokio::test]
2449 async fn test_write_behind_basic_write_and_flush() {
2450 use std::sync::atomic::{AtomicUsize, Ordering};
2451 let counter = Arc::new(AtomicUsize::new(0));
2453 let counter_clone = counter.clone();
2454 let on_flush: FlushCallback = Arc::new(move |ops: Vec<WriteOp>| {
2455 let c = counter_clone.clone();
2456 Box::pin(async move {
2457 c.fetch_add(ops.len(), Ordering::SeqCst);
2458 Ok(())
2459 })
2460 });
2461 let backend = Arc::new(InMemoryBackend::new());
2462 let writer = WriteBehindWriter::new(backend.clone(), on_flush);
2463
2464 writer.write(b"k1", b"v1", None).await.unwrap();
2466 writer.write(b"k2", b"v2", None).await.unwrap();
2467 writer.write(b"k3", b"v3", None).await.unwrap();
2468
2469 let v1 = backend.get("k1").await.unwrap();
2471 assert_eq!(v1, Some(b"v1".to_vec()));
2472
2473 assert_eq!(writer.pending_count().await, 3);
2475
2476 writer.flush().await.unwrap();
2478 assert_eq!(counter.load(Ordering::SeqCst), 3);
2479 assert_eq!(writer.pending_count().await, 0);
2480 }
2481
2482 #[tokio::test]
2483 async fn test_write_behind_delete() {
2484 let on_flush: FlushCallback =
2485 Arc::new(|_ops: Vec<WriteOp>| Box::pin(async move { Ok(()) }));
2486 let backend = Arc::new(InMemoryBackend::new());
2487 let writer = WriteBehindWriter::new(backend.clone(), on_flush);
2488
2489 writer.write(b"k1", b"v1", None).await.unwrap();
2491 assert!(backend.get("k1").await.unwrap().is_some());
2492 writer.delete(b"k1").await.unwrap();
2493 assert!(backend.get("k1").await.unwrap().is_none());
2495
2496 writer.flush().await.unwrap();
2498 assert_eq!(writer.pending_count().await, 0);
2499 }
2500
2501 #[tokio::test]
2502 async fn test_write_behind_flush_failure_retries() {
2503 let on_flush: FlushCallback = Arc::new(|_ops: Vec<WriteOp>| {
2505 Box::pin(async move { Err(CacheError::Internal("backend down".to_string())) })
2506 });
2507 let backend = Arc::new(InMemoryBackend::new());
2508 let writer = WriteBehindWriter::new(backend.clone(), on_flush);
2509
2510 writer.write(b"k1", b"v1", None).await.unwrap();
2511 let result = writer.flush().await;
2513 assert!(result.is_err());
2514 assert_eq!(writer.pending_count().await, 1);
2515 }
2516
2517 #[tokio::test]
2518 async fn test_write_behind_empty_flush_noop() {
2519 let on_flush: FlushCallback =
2520 Arc::new(|_ops: Vec<WriteOp>| Box::pin(async move { Ok(()) }));
2521 let backend = Arc::new(InMemoryBackend::new());
2522 let writer = WriteBehindWriter::new(backend, on_flush);
2523 writer.flush().await.unwrap();
2525 assert_eq!(writer.pending_count().await, 0);
2526 }
2527
2528 #[tokio::test]
2529 async fn test_write_behind_error_callback_invoked() {
2530 use std::sync::atomic::{AtomicUsize, Ordering};
2531 let error_counter = Arc::new(AtomicUsize::new(0));
2532 let ec = error_counter.clone();
2533 let on_error: ErrorCallback = Arc::new(move |_ops, _err| {
2534 ec.fetch_add(1, Ordering::SeqCst);
2535 });
2536 let on_flush: FlushCallback = Arc::new(|_ops: Vec<WriteOp>| {
2537 Box::pin(async move { Err(CacheError::Internal("fail".to_string())) })
2538 });
2539 let backend = Arc::new(InMemoryBackend::new());
2540 let writer = WriteBehindWriter::new(backend, on_flush).with_error_callback(on_error);
2541
2542 writer.write(b"k1", b"v1", None).await.unwrap();
2543 let _ = writer.flush().await;
2544 assert_eq!(error_counter.load(Ordering::SeqCst), 1);
2545 }
2546
2547 #[tokio::test]
2550 async fn test_query_cache_hit() {
2551 use std::collections::HashMap;
2552
2553 let cache = L2Cache::new();
2554 let mut call_count = 0;
2555
2556 let rows1 = cache
2558 .get_or_load_query(
2559 "users",
2560 "SELECT * FROM users WHERE status = ?",
2561 &[crate::value::Value::I64(1)],
2562 std::time::Duration::from_secs(300),
2563 || {
2564 call_count += 1;
2565 async {
2566 let mut row = HashMap::new();
2567 row.insert("id".to_string(), crate::value::Value::I64(1));
2568 row.insert(
2569 "name".to_string(),
2570 crate::value::Value::String("Alice".to_string()),
2571 );
2572 Ok(vec![row])
2573 }
2574 },
2575 )
2576 .await
2577 .unwrap();
2578
2579 assert_eq!(call_count, 1, "第一次查询应调用 loader");
2580 assert_eq!(rows1.len(), 1, "应返回 1 行");
2581
2582 let rows2 = cache
2584 .get_or_load_query(
2585 "users",
2586 "SELECT * FROM users WHERE status = ?",
2587 &[crate::value::Value::I64(1)],
2588 std::time::Duration::from_secs(300),
2589 || {
2590 call_count += 1;
2591 async { Ok(vec![]) }
2592 },
2593 )
2594 .await
2595 .unwrap();
2596
2597 assert_eq!(call_count, 1, "第二次查询不应调用 loader(缓存命中)");
2598 assert_eq!(rows2.len(), 1, "应返回缓存的 1 行");
2599 }
2600
2601 #[tokio::test]
2602 async fn test_query_cache_empty_result_cached() {
2603 let cache = L2Cache::new();
2604 let mut call_count = 0;
2605
2606 let rows1 = cache
2608 .get_or_load_query(
2609 "users",
2610 "SELECT * FROM users WHERE status = ?",
2611 &[crate::value::Value::I64(999)],
2612 std::time::Duration::from_secs(300),
2613 || {
2614 call_count += 1;
2615 async { Ok(vec![]) }
2616 },
2617 )
2618 .await
2619 .unwrap();
2620
2621 assert_eq!(call_count, 1, "第一次查询应调用 loader");
2622 assert_eq!(rows1.len(), 0, "应返回空结果");
2623
2624 let rows2 = cache
2626 .get_or_load_query(
2627 "users",
2628 "SELECT * FROM users WHERE status = ?",
2629 &[crate::value::Value::I64(999)],
2630 std::time::Duration::from_secs(300),
2631 || {
2632 call_count += 1;
2633 async { Ok(vec![]) }
2634 },
2635 )
2636 .await
2637 .unwrap();
2638
2639 assert_eq!(call_count, 1, "第二次查询不应调用 loader(空结果缓存命中)");
2640 assert_eq!(rows2.len(), 0, "应返回缓存的空结果");
2641 }
2642
2643 #[tokio::test]
2644 async fn test_query_cache_different_params() {
2645 use std::collections::HashMap;
2646
2647 let cache = L2Cache::new();
2648 let mut call_count = 0;
2649
2650 let _ = cache
2652 .get_or_load_query(
2653 "users",
2654 "SELECT * FROM users WHERE status = ?",
2655 &[crate::value::Value::I64(1)],
2656 std::time::Duration::from_secs(300),
2657 || {
2658 call_count += 1;
2659 async {
2660 let mut row = HashMap::new();
2661 row.insert("id".to_string(), crate::value::Value::I64(1));
2662 Ok(vec![row])
2663 }
2664 },
2665 )
2666 .await
2667 .unwrap();
2668
2669 let rows2 = cache
2671 .get_or_load_query(
2672 "users",
2673 "SELECT * FROM users WHERE status = ?",
2674 &[crate::value::Value::I64(2)],
2675 std::time::Duration::from_secs(300),
2676 || {
2677 call_count += 1;
2678 async {
2679 let mut row = HashMap::new();
2680 row.insert("id".to_string(), crate::value::Value::I64(2));
2681 row.insert(
2682 "name".to_string(),
2683 crate::value::Value::String("Bob".to_string()),
2684 );
2685 Ok(vec![row])
2686 }
2687 },
2688 )
2689 .await
2690 .unwrap();
2691
2692 assert_eq!(call_count, 2, "不同参数应调用 loader 两次");
2693 assert_eq!(rows2.len(), 1, "应返回 1 行");
2694 }
2695
2696 #[tokio::test]
2697 async fn test_query_cache_invalidate() {
2698 use std::collections::HashMap;
2699
2700 let cache = L2Cache::new();
2701 let mut call_count = 0;
2702
2703 let _ = cache
2705 .get_or_load_query(
2706 "users",
2707 "SELECT * FROM users WHERE status = ?",
2708 &[crate::value::Value::I64(1)],
2709 std::time::Duration::from_secs(300),
2710 || {
2711 call_count += 1;
2712 async {
2713 let mut row = HashMap::new();
2714 row.insert("id".to_string(), crate::value::Value::I64(1));
2715 Ok(vec![row])
2716 }
2717 },
2718 )
2719 .await
2720 .unwrap();
2721
2722 assert_eq!(call_count, 1, "第一次查询应调用 loader");
2723
2724 cache.invalidate_query(
2726 "users",
2727 "SELECT * FROM users WHERE status = ?",
2728 &[crate::value::Value::I64(1)],
2729 );
2730
2731 let _ = cache
2733 .get_or_load_query(
2734 "users",
2735 "SELECT * FROM users WHERE status = ?",
2736 &[crate::value::Value::I64(1)],
2737 std::time::Duration::from_secs(300),
2738 || {
2739 call_count += 1;
2740 async { Ok(vec![]) }
2741 },
2742 )
2743 .await
2744 .unwrap();
2745
2746 assert_eq!(call_count, 2, "失效后应重新调用 loader");
2747 }
2748
2749 #[tokio::test]
2750 async fn test_query_cache_hit_rate() {
2751 use std::collections::HashMap;
2752
2753 let cache = L2Cache::new();
2754 let mut call_count = 0;
2755
2756 for _ in 0..10 {
2758 let _ = cache
2759 .get_or_load_query(
2760 "users",
2761 "SELECT * FROM users WHERE status = ?",
2762 &[crate::value::Value::I64(1)],
2763 std::time::Duration::from_secs(300),
2764 || {
2765 call_count += 1;
2766 async {
2767 let mut row = HashMap::new();
2768 row.insert("id".to_string(), crate::value::Value::I64(1));
2769 Ok(vec![row])
2770 }
2771 },
2772 )
2773 .await
2774 .unwrap();
2775 }
2776
2777 assert_eq!(call_count, 1, "10 次查询中只有 1 次调用 loader");
2779
2780 let stats = cache.stats();
2781 assert_eq!(stats.hits, 9, "应命中 9 次");
2782 assert_eq!(stats.misses, 1, "应未命中 1 次");
2783
2784 let hit_rate = stats.hit_rate();
2785 assert!(
2786 hit_rate >= 0.8,
2787 "命中率应 >= 80%,实际: {:.2}%",
2788 hit_rate * 100.0
2789 );
2790 }
2791
2792 #[cfg(feature = "redis")]
2795 #[tokio::test]
2796 async fn test_redis_backend_invalid_url_returns_error() {
2797 let result = RedisBackend::new("not-a-valid-redis-url").await;
2799 let msg = match result {
2800 Ok(_) => panic!("无效 URL 不应连接成功"),
2801 Err(CacheError::Internal(m)) => m,
2802 Err(other) => panic!("期望 CacheError::Internal,实际: {:?}", other),
2803 };
2804 assert!(
2805 msg.contains("Redis client create failed"),
2806 "错误消息应指明 client 创建失败: {}",
2807 msg
2808 );
2809 }
2810}
2811
2812#[cfg(feature = "zero-copy")]
2821pub mod zero_copy {
2822 use crate::value::Value;
2823 use crate::value_borrowed::BorrowedValue;
2824
2825 pub fn to_borrowed(value: &Value) -> BorrowedValue<'_> {
2827 match value {
2828 &Value::Null => BorrowedValue::Null,
2829 Value::Bool(b) => BorrowedValue::Bool(*b),
2830 Value::I8(v) => BorrowedValue::I8(*v),
2831 Value::I16(v) => BorrowedValue::I16(*v),
2832 Value::I32(v) => BorrowedValue::I32(*v),
2833 Value::I64(v) => BorrowedValue::I64(*v),
2834 Value::U8(v) => BorrowedValue::U8(*v),
2835 Value::U16(v) => BorrowedValue::U16(*v),
2836 Value::U32(v) => BorrowedValue::U32(*v),
2837 Value::U64(v) => BorrowedValue::U64(*v),
2838 Value::F32(v) => BorrowedValue::F32(*v),
2839 Value::F64(v) => BorrowedValue::F64(*v),
2840 Value::Decimal(s) => BorrowedValue::Decimal(std::borrow::Cow::Borrowed(s)),
2841 Value::String(s) => BorrowedValue::String(std::borrow::Cow::Borrowed(s)),
2842 Value::Bytes(b) => BorrowedValue::Bytes(std::borrow::Cow::Borrowed(b)),
2843 Value::Uuid(s) => BorrowedValue::Uuid(std::borrow::Cow::Borrowed(s)),
2844 Value::Date(s) => BorrowedValue::Date(std::borrow::Cow::Borrowed(s)),
2845 Value::DateTime(s) => BorrowedValue::DateTime(std::borrow::Cow::Borrowed(s)),
2846 Value::Time(s) => BorrowedValue::Time(std::borrow::Cow::Borrowed(s)),
2847 Value::Json(s) => BorrowedValue::Json(std::borrow::Cow::Borrowed(s)),
2848 Value::Array(arr) => {
2849 let borrowed: Vec<BorrowedValue<'_>> = arr.iter().map(to_borrowed).collect();
2850 BorrowedValue::Array(borrowed)
2851 }
2852 Value::Object(obj) => {
2853 let mut borrowed = std::collections::HashMap::new();
2854 for (k, v) in obj {
2855 borrowed.insert(k.clone(), to_borrowed(v));
2856 }
2857 BorrowedValue::Object(borrowed)
2858 }
2859 #[cfg(feature = "perf-box-str")]
2860 Value::BoxedStr(s) => BorrowedValue::String(std::borrow::Cow::Borrowed(&**s)),
2861 }
2862 }
2863
2864 pub fn from_borrowed(borrowed: &BorrowedValue<'_>) -> Value {
2866 match borrowed {
2867 BorrowedValue::Null => Value::Null,
2868 BorrowedValue::Bool(b) => Value::Bool(*b),
2869 BorrowedValue::I8(v) => Value::I8(*v),
2870 BorrowedValue::I16(v) => Value::I16(*v),
2871 BorrowedValue::I32(v) => Value::I32(*v),
2872 BorrowedValue::I64(v) => Value::I64(*v),
2873 BorrowedValue::U8(v) => Value::U8(*v),
2874 BorrowedValue::U16(v) => Value::U16(*v),
2875 BorrowedValue::U32(v) => Value::U32(*v),
2876 BorrowedValue::U64(v) => Value::U64(*v),
2877 BorrowedValue::F32(v) => Value::F32(*v),
2878 BorrowedValue::F64(v) => Value::F64(*v),
2879 BorrowedValue::Decimal(s) => Value::Decimal(s.to_string()),
2880 BorrowedValue::String(s) => Value::String(s.to_string()),
2881 BorrowedValue::Bytes(b) => Value::Bytes(b.to_vec()),
2882 BorrowedValue::Uuid(s) => Value::Uuid(s.to_string()),
2883 BorrowedValue::Date(s) => Value::Date(s.to_string()),
2884 BorrowedValue::DateTime(s) => Value::DateTime(s.to_string()),
2885 BorrowedValue::Time(s) => Value::Time(s.to_string()),
2886 BorrowedValue::Json(s) => Value::Json(s.to_string()),
2887 BorrowedValue::Array(arr) => {
2888 let values: Vec<Value> = arr.iter().map(from_borrowed).collect();
2889 Value::Array(values)
2890 }
2891 BorrowedValue::Object(obj) => {
2892 let mut values = std::collections::HashMap::new();
2893 for (k, v) in obj {
2894 values.insert(k.clone(), from_borrowed(v));
2895 }
2896 Value::Object(values)
2897 }
2898 }
2899 }
2900
2901 pub fn serialize_zero_copy(value: &Value) -> Vec<u8> {
2903 let borrowed = to_borrowed(value);
2904 format!("{:?}", borrowed).into_bytes()
2905 }
2906
2907 pub fn deserialize_zero_copy(data: &[u8]) -> Result<Value, String> {
2909 let s = std::str::from_utf8(data).map_err(|e| e.to_string())?;
2910 Ok(Value::String(s.to_string()))
2911 }
2912}
2913
2914#[cfg(all(test, feature = "zero-copy"))]
2915mod zero_copy_tests {
2916 use super::zero_copy::*;
2917 use crate::value::Value;
2918 use crate::value_borrowed::BorrowedValue;
2919 use std::borrow::Cow;
2920
2921 #[test]
2922 fn test_to_borrowed_roundtrip() {
2923 let values = vec![
2924 Value::Null,
2925 Value::Bool(true),
2926 Value::I64(42),
2927 Value::F64(3.14),
2928 Value::String("hello".to_string()),
2929 Value::I32(-100),
2930 ];
2931 for v in &values {
2932 let borrowed = to_borrowed(v);
2933 let restored = from_borrowed(&borrowed);
2934 assert_eq!(*v, restored);
2935 }
2936 }
2937
2938 #[test]
2939 fn test_serialize_deserialize() {
2940 let v = Value::String("test".to_string());
2941 let data = serialize_zero_copy(&v);
2942 assert!(!data.is_empty());
2943 }
2944
2945 #[test]
2946 fn test_zero_copy_no_allocation() {
2947 let v = Value::String("hello".to_string());
2948 let borrowed = to_borrowed(&v);
2949 match &borrowed {
2950 BorrowedValue::String(Cow::Borrowed(s)) => assert_eq!(*s, "hello"),
2951 _ => panic!("应为 Cow::Borrowed"),
2952 }
2953 }
2954}
2955
2956#[cfg(all(test, feature = "prod-redis-tls"))]
2957mod prod_redis_tls_tests {
2958 use super::*;
2959
2960 #[test]
2961 fn test_tls_config_validate_production_rejects_skip_verify() {
2962 let tls = RedisTlsConfig {
2963 enabled: true,
2964 ca_cert_path: None,
2965 client_cert_path: None,
2966 client_key_path: None,
2967 sni: Some("redis.example.com".to_string()),
2968 skip_verify: true,
2969 };
2970 let result = tls.validate(true);
2971 assert!(result.is_err());
2972 assert!(result
2973 .unwrap_err()
2974 .contains("skip_verify forbidden in production"));
2975 }
2976
2977 #[test]
2978 fn test_tls_config_validate_development_allows_skip_verify() {
2979 let tls = RedisTlsConfig {
2980 enabled: true,
2981 ca_cert_path: None,
2982 client_cert_path: None,
2983 client_key_path: None,
2984 sni: Some("redis.example.com".to_string()),
2985 skip_verify: true,
2986 };
2987 assert!(tls.validate(false).is_ok());
2988 }
2989
2990 #[test]
2991 fn test_tls_config_disabled_validate_passes() {
2992 let tls = RedisTlsConfig::disabled();
2993 assert!(tls.validate(true).is_ok());
2994 }
2995
2996 #[test]
2997 fn test_mask_redis_url_with_password() {
2998 let masked = mask_redis_url("redis://:test123@127.0.0.1:6379/0");
2999 assert_eq!(masked, "redis://:***@127.0.0.1:6379/0");
3000 }
3001
3002 #[test]
3003 fn test_mask_redis_url_without_password() {
3004 let masked = mask_redis_url("redis://127.0.0.1:6379/0");
3005 assert_eq!(masked, "redis://127.0.0.1:6379/0");
3006 }
3007
3008 #[test]
3009 fn test_mask_redis_url_empty_password() {
3010 let masked = mask_redis_url("redis://:@127.0.0.1:6379/0");
3011 assert_eq!(masked, "redis://:@127.0.0.1:6379/0");
3012 }
3013}