1use std::collections::hash_map::DefaultHasher;
52use std::hash::{Hash, Hasher};
53use std::sync::Arc;
54use std::time::{Duration, SystemTime};
55
56use parking_lot::RwLock;
57use redis::aio::ConnectionManager;
58use redis::{AsyncCommands, Client};
59use serde::{Deserialize, Serialize};
60use thiserror::Error;
61use tokio::sync::Mutex;
62use tracing::{debug, error, info, warn};
63
64use futures::StreamExt;
65use scirs2_core::metrics::{Counter, MetricsRegistry};
66
67#[derive(Error, Debug)]
69pub enum DistributedCacheError {
70 #[error("Redis connection error: {0}")]
71 RedisConnection(#[from] redis::RedisError),
72
73 #[error("Serialization error: {0}")]
74 Serialization(String),
75
76 #[error("Deserialization error: {0}")]
77 Deserialization(String),
78
79 #[error("Compression error: {0}")]
80 Compression(String),
81
82 #[error("Decompression error: {0}")]
83 Decompression(String),
84
85 #[error("Invalid configuration: {0}")]
86 InvalidConfig(String),
87
88 #[error("Cache operation failed: {0}")]
89 OperationFailed(String),
90}
91
92pub type Result<T> = std::result::Result<T, DistributedCacheError>;
93
94#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
96pub struct CacheKey {
97 pub id: String,
99 pub namespace: Option<String>,
101}
102
103impl CacheKey {
104 pub fn new(id: String) -> Self {
106 Self {
107 id,
108 namespace: None,
109 }
110 }
111
112 pub fn with_namespace(id: String, namespace: String) -> Self {
114 Self {
115 id,
116 namespace: Some(namespace),
117 }
118 }
119
120 pub fn hash(&self) -> u64 {
122 let mut hasher = DefaultHasher::new();
123 self.id.hash(&mut hasher);
124 if let Some(ref ns) = self.namespace {
125 ns.hash(&mut hasher);
126 }
127 hasher.finish()
128 }
129
130 pub fn redis_key(&self) -> String {
132 match &self.namespace {
133 Some(ns) => format!("oxirs:cache:{}:{}", ns, self.id),
134 None => format!("oxirs:cache:{}", self.id),
135 }
136 }
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct CacheValue {
142 pub data: Vec<u8>,
144 pub created_at: SystemTime,
146 pub metadata: Option<String>,
148}
149
150impl CacheValue {
151 pub fn new(data: Vec<u8>) -> Self {
153 Self {
154 data,
155 created_at: SystemTime::now(),
156 metadata: None,
157 }
158 }
159
160 pub fn with_metadata(data: Vec<u8>, metadata: String) -> Self {
162 Self {
163 data,
164 created_at: SystemTime::now(),
165 metadata: Some(metadata),
166 }
167 }
168}
169
170#[derive(Debug, Clone)]
172struct L1Entry {
173 value: CacheValue,
174 expires_at: SystemTime,
175}
176
177impl L1Entry {
178 fn new(value: CacheValue, ttl_seconds: u64) -> Self {
179 let expires_at = SystemTime::now() + Duration::from_secs(ttl_seconds);
180 Self { value, expires_at }
181 }
182
183 fn is_expired(&self) -> bool {
184 SystemTime::now() > self.expires_at
185 }
186}
187
188#[derive(Debug, Clone)]
190pub struct DistributedCacheConfig {
191 pub l1_max_size: usize,
193 pub l1_ttl_seconds: u64,
195 pub l2_redis_url: String,
197 pub l2_ttl_seconds: u64,
199 pub compression: bool,
201 pub invalidation_channel: String,
203}
204
205impl Default for DistributedCacheConfig {
206 fn default() -> Self {
207 Self {
208 l1_max_size: 1000,
209 l1_ttl_seconds: 300,
210 l2_redis_url: "redis://localhost:6379".to_string(),
211 l2_ttl_seconds: 3600,
212 compression: true,
213 invalidation_channel: "oxirs:cache:invalidate".to_string(),
214 }
215 }
216}
217
218#[derive(Clone)]
220pub struct DistributedCacheMetrics {
221 pub l1_hits: Arc<Counter>,
222 pub l1_misses: Arc<Counter>,
223 pub l2_hits: Arc<Counter>,
224 pub l2_misses: Arc<Counter>,
225 pub invalidations_sent: Arc<Counter>,
226 pub invalidations_received: Arc<Counter>,
227 pub compression_ratio: Arc<RwLock<f64>>,
228}
229
230impl DistributedCacheMetrics {
231 fn new(_registry: &MetricsRegistry) -> Self {
232 Self {
233 l1_hits: Arc::new(Counter::new("distributed_cache_l1_hits".to_string())),
234 l1_misses: Arc::new(Counter::new("distributed_cache_l1_misses".to_string())),
235 l2_hits: Arc::new(Counter::new("distributed_cache_l2_hits".to_string())),
236 l2_misses: Arc::new(Counter::new("distributed_cache_l2_misses".to_string())),
237 invalidations_sent: Arc::new(Counter::new(
238 "distributed_cache_invalidations_sent".to_string(),
239 )),
240 invalidations_received: Arc::new(Counter::new(
241 "distributed_cache_invalidations_received".to_string(),
242 )),
243 compression_ratio: Arc::new(RwLock::new(1.0)),
244 }
245 }
246
247 pub fn l1_hit_rate(&self) -> f64 {
249 let hits = self.l1_hits.get() as f64;
250 let total = hits + self.l1_misses.get() as f64;
251 if total > 0.0 {
252 hits / total
253 } else {
254 0.0
255 }
256 }
257
258 pub fn l2_hit_rate(&self) -> f64 {
260 let hits = self.l2_hits.get() as f64;
261 let total = hits + self.l2_misses.get() as f64;
262 if total > 0.0 {
263 hits / total
264 } else {
265 0.0
266 }
267 }
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct InvalidationMessage {
273 pub key: CacheKey,
274 pub timestamp: SystemTime,
275 pub sender_id: String,
276}
277
278pub struct DistributedCache {
280 l1_cache: Arc<RwLock<lru::LruCache<CacheKey, L1Entry>>>,
281 l2_client: ConnectionManager,
282 pubsub_client: Arc<Mutex<Client>>,
283 config: DistributedCacheConfig,
284 metrics: DistributedCacheMetrics,
285 node_id: String,
286}
287
288impl DistributedCache {
289 pub async fn new(config: DistributedCacheConfig) -> Result<Self> {
291 let registry = MetricsRegistry::new();
292 Self::new_with_registry(config, ®istry).await
293 }
294
295 pub async fn new_with_registry(
297 config: DistributedCacheConfig,
298 registry: &MetricsRegistry,
299 ) -> Result<Self> {
300 if config.l1_max_size == 0 {
302 return Err(DistributedCacheError::InvalidConfig(
303 "l1_max_size must be greater than 0".to_string(),
304 ));
305 }
306
307 let client = Client::open(config.l2_redis_url.as_str())?;
309 let l2_client = ConnectionManager::new(client.clone()).await?;
310
311 let l1_cache = Arc::new(RwLock::new(lru::LruCache::new(
313 std::num::NonZeroUsize::new(config.l1_max_size).ok_or_else(|| {
314 DistributedCacheError::InvalidConfig(
315 "l1_max_size must be greater than 0".to_string(),
316 )
317 })?,
318 )));
319
320 let node_id = uuid::Uuid::new_v4().to_string();
322
323 info!(
324 "Distributed cache initialized: node_id={}, l1_size={}, l2_url={}",
325 node_id, config.l1_max_size, config.l2_redis_url
326 );
327
328 Ok(Self {
329 l1_cache,
330 l2_client,
331 pubsub_client: Arc::new(Mutex::new(client)),
332 config,
333 metrics: DistributedCacheMetrics::new(registry),
334 node_id,
335 })
336 }
337
338 pub async fn get(&self, key: &CacheKey) -> Result<Option<CacheValue>> {
340 {
342 let mut l1 = self.l1_cache.write();
343 if let Some(entry) = l1.get(key) {
344 if !entry.is_expired() {
345 self.metrics.l1_hits.inc();
346 debug!("L1 cache hit: key={:?}", key);
347 return Ok(Some(entry.value.clone()));
348 } else {
349 l1.pop(key);
351 }
352 }
353 }
354 self.metrics.l1_misses.inc();
355 debug!("L1 cache miss: key={:?}", key);
356
357 let redis_key = key.redis_key();
359 let mut conn = self.l2_client.clone();
360
361 let redis_timeout = Duration::from_secs(5);
363 let redis_get = async { conn.get::<_, Option<Vec<u8>>>(&redis_key).await };
364
365 match tokio::time::timeout(redis_timeout, redis_get).await {
366 Ok(Ok(Some(redis_value))) => {
367 self.metrics.l2_hits.inc();
368 debug!("L2 cache hit: key={:?}", key);
369
370 match self.deserialize_value(&redis_value) {
371 Ok(value) => {
372 {
374 let mut l1 = self.l1_cache.write();
375 let entry = L1Entry::new(value.clone(), self.config.l1_ttl_seconds);
376 l1.put(key.clone(), entry);
377 }
378
379 Ok(Some(value))
380 }
381 Err(e) => {
382 error!("Failed to deserialize L2 value: {:?}", e);
383 Err(e)
384 }
385 }
386 }
387 Ok(Ok(None)) => {
388 self.metrics.l2_misses.inc();
389 debug!("L2 cache miss: key={:?}", key);
390 Ok(None)
391 }
392 Ok(Err(e)) => {
393 error!("Redis get error: {:?}", e);
394 Err(DistributedCacheError::RedisConnection(e))
395 }
396 Err(_) => {
397 error!("Redis get timeout for key={:?}", key);
398 self.metrics.l2_misses.inc();
399 Err(DistributedCacheError::OperationFailed(
400 "Redis get operation timed out".to_string(),
401 ))
402 }
403 }
404 }
405
406 pub async fn put(&self, key: CacheKey, value: CacheValue) -> Result<()> {
408 {
410 let mut l1 = self.l1_cache.write();
411 let entry = L1Entry::new(value.clone(), self.config.l1_ttl_seconds);
412 l1.put(key.clone(), entry);
413 }
414 debug!("Put in L1: key={:?}", key);
415
416 let redis_key = key.redis_key();
418 let redis_value = self.serialize_value(&value)?;
419
420 let mut conn = self.l2_client.clone();
421 let redis_timeout = Duration::from_secs(5);
422 let redis_set = async {
423 conn.set_ex::<_, _, ()>(&redis_key, &redis_value, self.config.l2_ttl_seconds)
424 .await
425 };
426
427 match tokio::time::timeout(redis_timeout, redis_set).await {
428 Ok(Ok(_)) => {
429 debug!("Put in L2: key={:?}", key);
430 Ok(())
431 }
432 Ok(Err(e)) => {
433 error!("Redis set error: {:?}", e);
434 Err(DistributedCacheError::RedisConnection(e))
435 }
436 Err(_) => {
437 error!("Redis set timeout for key={:?}", key);
438 Err(DistributedCacheError::OperationFailed(
439 "Redis set operation timed out".to_string(),
440 ))
441 }
442 }
443 }
444
445 pub async fn invalidate(&self, key: &CacheKey) -> Result<()> {
447 {
449 let mut l1 = self.l1_cache.write();
450 l1.pop(key);
451 }
452 debug!("Invalidated in L1: key={:?}", key);
453
454 let redis_key = key.redis_key();
456 let mut conn = self.l2_client.clone();
457 let redis_timeout = Duration::from_secs(5);
458 let redis_del = async { conn.del::<_, ()>(&redis_key).await };
459
460 match tokio::time::timeout(redis_timeout, redis_del).await {
461 Ok(Ok(_)) => {
462 debug!("Invalidated in L2: key={:?}", key);
463 }
464 Ok(Err(e)) => {
465 error!("Redis del error: {:?}", e);
466 return Err(DistributedCacheError::RedisConnection(e));
467 }
468 Err(_) => {
469 error!("Redis del timeout for key={:?}", key);
470 return Err(DistributedCacheError::OperationFailed(
471 "Redis del operation timed out".to_string(),
472 ));
473 }
474 }
475
476 let message = InvalidationMessage {
478 key: key.clone(),
479 timestamp: SystemTime::now(),
480 sender_id: self.node_id.clone(),
481 };
482 self.publish_invalidation(message).await?;
483
484 self.metrics.invalidations_sent.inc();
485 Ok(())
486 }
487
488 pub async fn start_invalidation_listener(&self) -> Result<()> {
490 let l1_cache = self.l1_cache.clone();
491 let pubsub_client = self.pubsub_client.clone();
492 let channel = self.config.invalidation_channel.clone();
493 let metrics = self.metrics.clone();
494 let node_id = self.node_id.clone();
495 let channel_for_log = channel.clone();
496
497 tokio::spawn(async move {
498 loop {
499 let channel_clone = channel.clone();
500 let node_id_clone = node_id.clone();
501 match Self::run_invalidation_listener(
502 l1_cache.clone(),
503 pubsub_client.clone(),
504 channel_clone,
505 metrics.clone(),
506 node_id_clone,
507 )
508 .await
509 {
510 Ok(_) => {
511 warn!("Invalidation listener stopped, restarting...");
512 }
513 Err(e) => {
514 error!("Invalidation listener error: {:?}, restarting...", e);
515 }
516 }
517 tokio::time::sleep(Duration::from_secs(5)).await;
518 }
519 });
520
521 info!(
522 "Started invalidation listener on channel: {}",
523 channel_for_log
524 );
525 Ok(())
526 }
527
528 async fn run_invalidation_listener(
529 l1_cache: Arc<RwLock<lru::LruCache<CacheKey, L1Entry>>>,
530 pubsub_client: Arc<Mutex<Client>>,
531 channel: String,
532 metrics: DistributedCacheMetrics,
533 node_id: String,
534 ) -> Result<()> {
535 let client = pubsub_client.lock().await;
536 let mut pubsub = client.get_async_pubsub().await?;
537 pubsub.subscribe(&channel).await?;
538
539 let mut stream = pubsub.on_message();
540 while let Some(msg) = stream.next().await {
541 let payload: String = match msg.get_payload() {
542 Ok(p) => p,
543 Err(e) => {
544 error!("Failed to get message payload: {:?}", e);
545 continue;
546 }
547 };
548
549 match serde_json::from_str::<InvalidationMessage>(&payload) {
550 Ok(inv_msg) => {
551 if inv_msg.sender_id == node_id {
553 continue;
554 }
555
556 {
558 let mut l1 = l1_cache.write();
559 l1.pop(&inv_msg.key);
560 }
561
562 metrics.invalidations_received.inc();
563 debug!("Received invalidation: key={:?}", inv_msg.key);
564 }
565 Err(e) => {
566 error!("Failed to deserialize invalidation message: {:?}", e);
567 }
568 }
569 }
570
571 Ok(())
572 }
573
574 async fn publish_invalidation(&self, message: InvalidationMessage) -> Result<()> {
575 let payload = serde_json::to_string(&message).map_err(|e| {
576 DistributedCacheError::Serialization(format!("Failed to serialize message: {}", e))
577 })?;
578
579 let mut conn = self.l2_client.clone();
580 let redis_timeout = Duration::from_secs(5);
581 let redis_publish = async {
582 conn.publish::<_, _, ()>(&self.config.invalidation_channel, &payload)
583 .await
584 };
585
586 match tokio::time::timeout(redis_timeout, redis_publish).await {
587 Ok(Ok(_)) => {
588 debug!(
589 "Published invalidation: key={:?}, channel={}",
590 message.key, self.config.invalidation_channel
591 );
592 Ok(())
593 }
594 Ok(Err(e)) => {
595 error!("Redis publish error: {:?}", e);
596 Err(DistributedCacheError::RedisConnection(e))
597 }
598 Err(_) => {
599 error!("Redis publish timeout");
600 Err(DistributedCacheError::OperationFailed(
601 "Redis publish operation timed out".to_string(),
602 ))
603 }
604 }
605 }
606
607 fn serialize_value(&self, value: &CacheValue) -> Result<Vec<u8>> {
608 let serialized = oxicode::serde::encode_to_vec(value, oxicode::config::standard())
609 .map_err(|e| {
610 DistributedCacheError::Serialization(format!("oxicode serialization failed: {}", e))
611 })?;
612
613 if self.config.compression && serialized.len() > 1024 {
614 let compressed = oxiarc_deflate::gzip_compress(&serialized, 1).map_err(|e| {
617 DistributedCacheError::Compression(format!("Compression failed: {}", e))
618 })?;
619
620 let ratio = serialized.len() as f64 / compressed.len() as f64;
622 *self.metrics.compression_ratio.write() = ratio;
623
624 debug!(
625 "Compressed value: original={}, compressed={}, ratio={:.2}x",
626 serialized.len(),
627 compressed.len(),
628 ratio
629 );
630
631 Ok(compressed)
632 } else {
633 Ok(serialized)
634 }
635 }
636
637 fn deserialize_value(&self, data: &[u8]) -> Result<CacheValue> {
638 let decompressed = if self.config.compression && data.len() > 1024 {
639 oxiarc_deflate::gzip_decompress(data).map_err(|e| {
641 DistributedCacheError::Decompression(format!("Decompression failed: {}", e))
642 })?
643 } else {
644 data.to_vec()
645 };
646
647 oxicode::serde::decode_from_slice(&decompressed, oxicode::config::standard())
648 .map(|(value, _)| value)
649 .map_err(|e| {
650 DistributedCacheError::Deserialization(format!(
651 "oxicode deserialization failed: {}",
652 e
653 ))
654 })
655 }
656
657 pub fn metrics(&self) -> &DistributedCacheMetrics {
659 &self.metrics
660 }
661
662 pub fn clear_l1(&self) {
664 let mut l1 = self.l1_cache.write();
665 l1.clear();
666 info!("Cleared L1 cache");
667 }
668
669 pub fn l1_size(&self) -> usize {
671 let l1 = self.l1_cache.read();
672 l1.len()
673 }
674}
675
676#[cfg(test)]
677mod tests {
678 use super::*;
679
680 #[test]
681 fn test_cache_key_hash() {
682 let key1 = CacheKey::new("query1".to_string());
683 let key2 = CacheKey::new("query1".to_string());
684 assert_eq!(key1.hash(), key2.hash());
685
686 let key3 = CacheKey::new("query2".to_string());
687 assert_ne!(key1.hash(), key3.hash());
688 }
689
690 #[test]
691 fn test_cache_key_redis_key() {
692 let key = CacheKey::new("query1".to_string());
693 assert_eq!(key.redis_key(), "oxirs:cache:query1");
694
695 let key_ns = CacheKey::with_namespace("query1".to_string(), "tenant1".to_string());
696 assert_eq!(key_ns.redis_key(), "oxirs:cache:tenant1:query1");
697 }
698
699 #[test]
700 #[ignore = "inherently slow: requires wall-clock TTL expiry (use nextest --ignored to run)"]
701 fn test_l1_entry_expiration() {
702 let value = CacheValue::new(vec![1, 2, 3]);
703 let entry = L1Entry::new(value, 1);
704 assert!(!entry.is_expired());
705
706 std::thread::sleep(Duration::from_secs(2));
707 assert!(entry.is_expired());
708 }
709}