Skip to main content

multi_tier_cache/
cache_manager.rs

1//! Cache Manager - Unified Cache Operations
2//!
3//! Manages operations across L1 (Moka) and L2 (Redis) caches with intelligent fallback.
4
5use crate::error::CacheResult;
6use dashmap::DashMap;
7use rand::Rng;
8use std::future::Future;
9use std::sync::Arc;
10use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
11use std::time::Duration;
12use tokio::sync::watch;
13use tracing::{debug, error, info, warn};
14
15#[cfg(feature = "moka")]
16#[cfg_attr(docsrs, doc(cfg(feature = "moka")))]
17use crate::L1Cache;
18#[cfg(feature = "redis")]
19#[cfg_attr(docsrs, doc(cfg(feature = "redis")))]
20use crate::L2Cache;
21#[cfg(feature = "redis")]
22#[cfg_attr(docsrs, doc(cfg(feature = "redis")))]
23use crate::invalidation::{
24    AtomicInvalidationStats, InvalidationConfig, InvalidationMessage, InvalidationPublisher,
25    InvalidationSubscriber,
26};
27use crate::serialization::{CacheSerializer, JsonSerializer};
28use crate::traits::{CacheBackend, L2CacheBackend, StreamingBackend};
29use bytes::Bytes;
30use futures_util::future::BoxFuture;
31
32///// Type alias for the in-flight requests map
33/// Stores a watch sender for each active key computation
34type InFlightMap = DashMap<String, Arc<watch::Sender<Option<CacheResult<Option<Bytes>>>>>>;
35
36/// RAII Guard to ensure that keys are removed from `in_flight_requests` on cancellation/drop.
37struct RemoveInFlightGuard {
38    map: Arc<InFlightMap>,
39    key: String,
40}
41
42impl Drop for RemoveInFlightGuard {
43    fn drop(&mut self) {
44        self.map.remove(&self.key);
45    }
46}
47
48/// Represents the concurrency state for in-flight requests stampede protection.
49enum FlightState {
50    Creator(Arc<watch::Sender<Option<CacheResult<Option<Bytes>>>>>),
51    Waiter(watch::Receiver<Option<CacheResult<Option<Bytes>>>>),
52}
53
54/// Cache strategies for different data types
55#[derive(Debug, Clone)]
56#[allow(dead_code)]
57pub enum CacheStrategy {
58    /// Real-time data - 10 seconds TTL
59    RealTime,
60    /// Short-term data - 5 minutes TTL  
61    ShortTerm,
62    /// Medium-term data - 1 hour TTL
63    MediumTerm,
64    /// Long-term data - 3 hours TTL
65    LongTerm,
66    /// Custom TTL
67    Custom(Duration),
68    /// Default strategy (5 minutes)
69    Default,
70}
71
72impl CacheStrategy {
73    /// Convert strategy to duration
74    #[must_use]
75    pub fn to_duration(&self) -> Duration {
76        match self {
77            Self::RealTime => Duration::from_secs(10),
78            Self::ShortTerm | Self::Default => Duration::from_mins(5), // 5 minutes
79            Self::MediumTerm => Duration::from_hours(1),               // 1 hour
80            Self::LongTerm => Duration::from_hours(3),                 // 3 hours
81            Self::Custom(duration) => *duration,
82        }
83    }
84}
85
86/// Statistics for a single cache tier
87#[derive(Debug)]
88pub struct TierStats {
89    /// Tier level (1 = L1, 2 = L2, 3 = L3, etc.)
90    pub tier_level: usize,
91    /// Number of cache hits at this tier
92    pub hits: AtomicU64,
93    /// Backend name for identification
94    pub backend_name: String,
95}
96
97impl Clone for TierStats {
98    fn clone(&self) -> Self {
99        Self {
100            tier_level: self.tier_level,
101            hits: AtomicU64::new(self.hits.load(Ordering::Relaxed)),
102            backend_name: self.backend_name.clone(),
103        }
104    }
105}
106
107impl TierStats {
108    fn new(tier_level: usize, backend_name: String) -> Self {
109        Self {
110            tier_level,
111            hits: AtomicU64::new(0),
112            backend_name,
113        }
114    }
115
116    /// Get current hit count
117    pub fn hit_count(&self) -> u64 {
118        self.hits.load(Ordering::Relaxed)
119    }
120}
121
122/// A single cache tier in the multi-tier architecture
123#[derive(Clone)]
124pub struct CacheTier {
125    /// The backend for this tier
126    pub backend: Arc<dyn L2CacheBackend>,
127    /// Tier level (1 for fastest, increases for slower/cheaper tiers)
128    pub tier_level: usize,
129    /// Whether to promote keys FROM lower tiers TO this tier
130    pub promotion_enabled: bool,
131    /// Promotion frequency (N) - promote with 1/N probability
132    pub promotion_frequency: usize,
133    /// TTL multiplier for this tier (e.g., L2 might store for 2x L1 TTL)
134    pub ttl_scale: f64,
135    /// Statistics for this tier
136    pub stats: TierStats,
137}
138
139impl CacheTier {
140    /// Create a new cache tier
141    pub fn new(
142        backend: Arc<dyn L2CacheBackend>,
143        tier_level: usize,
144        promotion_enabled: bool,
145        promotion_frequency: usize,
146        ttl_scale: f64,
147    ) -> Self {
148        let backend_name = backend.name().to_string();
149        Self {
150            backend,
151            tier_level,
152            promotion_enabled,
153            promotion_frequency,
154            ttl_scale,
155            stats: TierStats::new(tier_level, backend_name),
156        }
157    }
158
159    /// Get value with TTL from this tier
160    async fn get_with_ttl(&self, key: &str) -> Option<(Bytes, Option<Duration>)> {
161        self.backend.get_with_ttl(key).await
162    }
163
164    /// Set value with TTL in this tier
165    async fn set_with_ttl(&self, key: &str, value: Bytes, ttl: Duration) -> CacheResult<()> {
166        let scaled_ttl = Duration::from_secs_f64(ttl.as_secs_f64() * self.ttl_scale);
167        self.backend.set_with_ttl(key, value, scaled_ttl).await
168    }
169
170    /// Remove value from this tier
171    async fn remove(&self, key: &str) -> CacheResult<()> {
172        self.backend.remove(key).await
173    }
174
175    /// Record a cache hit for this tier
176    fn record_hit(&self) {
177        self.stats.hits.fetch_add(1, Ordering::Relaxed);
178    }
179}
180
181/// Configuration for a cache tier (used in builder pattern)
182#[derive(Debug, Clone)]
183pub struct TierConfig {
184    /// Tier level (1, 2, 3, 4...)
185    pub tier_level: usize,
186    /// Enable promotion to upper tiers on hit
187    pub promotion_enabled: bool,
188    /// Promotion frequency (N) - promote with 1/N probability (default 10)
189    pub promotion_frequency: usize,
190    /// TTL scale factor (1.0 = same as base TTL)
191    pub ttl_scale: f64,
192}
193
194impl TierConfig {
195    /// Create new tier configuration
196    #[must_use]
197    pub fn new(tier_level: usize) -> Self {
198        Self {
199            tier_level,
200            promotion_enabled: true,
201            promotion_frequency: 10,
202            ttl_scale: 1.0,
203        }
204    }
205
206    /// Configure as L1 (hot tier)
207    #[must_use]
208    pub fn as_l1() -> Self {
209        Self {
210            tier_level: 1,
211            promotion_enabled: false, // L1 is already top tier
212            promotion_frequency: 1,   // Doesn't matter but use 1
213            ttl_scale: 1.0,
214        }
215    }
216
217    /// Configure as L2 (warm tier)
218    #[must_use]
219    pub fn as_l2() -> Self {
220        Self {
221            tier_level: 2,
222            promotion_enabled: true,
223            promotion_frequency: 10,
224            ttl_scale: 1.0,
225        }
226    }
227
228    /// Configure as L3 (cold tier) with longer TTL
229    #[must_use]
230    pub fn as_l3() -> Self {
231        Self {
232            tier_level: 3,
233            promotion_enabled: true,
234            promotion_frequency: 10,
235            ttl_scale: 2.0, // Keep data 2x longer
236        }
237    }
238
239    /// Configure as L4 (archive tier) with much longer TTL
240    #[must_use]
241    pub fn as_l4() -> Self {
242        Self {
243            tier_level: 4,
244            promotion_enabled: true,
245            promotion_frequency: 10,
246            ttl_scale: 8.0, // Keep data 8x longer
247        }
248    }
249
250    /// Set promotion enabled
251    #[must_use]
252    pub fn with_promotion(mut self, enabled: bool) -> Self {
253        self.promotion_enabled = enabled;
254        self
255    }
256
257    /// Set promotion frequency (N)
258    #[must_use]
259    pub fn with_promotion_frequency(mut self, n: usize) -> Self {
260        self.promotion_frequency = n;
261        self
262    }
263
264    /// Set TTL scale factor
265    #[must_use]
266    pub fn with_ttl_scale(mut self, scale: f64) -> Self {
267        self.ttl_scale = scale;
268        self
269    }
270
271    /// Set tier level
272    #[must_use]
273    pub fn with_level(mut self, level: usize) -> Self {
274        self.tier_level = level;
275        self
276    }
277}
278
279pub struct CacheManager {
280    /// Ordered list of cache tiers (L1, L2, L3, ...)
281    tiers: Vec<CacheTier>,
282
283    /// Optional streaming backend
284    streaming_backend: Option<Arc<dyn StreamingBackend>>,
285    /// Statistics
286    total_requests: AtomicU64,
287    l1_hits: AtomicU64,
288    l2_hits: AtomicU64,
289    misses: AtomicU64,
290    /// In-flight requests map (Broadcaster integration will replace this in Step 4)
291    in_flight_requests: Arc<InFlightMap>,
292    /// Pluggable serializer
293    serializer: Arc<CacheSerializer>,
294    /// Invalidation publisher
295    #[cfg(feature = "redis")]
296    #[cfg_attr(docsrs, doc(cfg(feature = "redis")))]
297    invalidation_publisher: Option<Arc<InvalidationPublisher>>,
298    /// Invalidation subscriber
299    #[cfg(feature = "redis")]
300    #[cfg_attr(docsrs, doc(cfg(feature = "redis")))]
301    invalidation_subscriber: Option<Arc<InvalidationSubscriber>>,
302    /// Invalidation statistics
303    #[cfg(feature = "redis")]
304    #[cfg_attr(docsrs, doc(cfg(feature = "redis")))]
305    invalidation_stats: Arc<AtomicInvalidationStats>,
306    /// Unique instance ID for filtering self-invalidation echo
307    instance_id: String,
308    /// Number of promotions performed
309    promotions: AtomicUsize,
310}
311
312impl CacheManager {
313    /// Create new cache manager with trait objects (pluggable backends)
314    ///
315    /// This is the primary constructor for v0.3.0+, supporting custom cache backends.
316    ///
317    /// # Arguments
318    ///
319    /// * `l1_cache` - Any L1 cache backend implementing `CacheBackend` trait
320    /// * `l2_cache` - Any L2 cache backend implementing `L2CacheBackend` trait
321    /// * `streaming_backend` - Optional streaming backend (None to disable streaming)
322    ///
323    /// # Example
324    ///
325    /// ```rust,no_run
326    /// # #[tokio::main]
327    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
328    /// use multi_tier_cache::{CacheManager, L1Cache, L2Cache, MokaCacheConfig};
329    /// use std::sync::Arc;
330    ///
331    /// let l1 = Arc::new(L1Cache::new(MokaCacheConfig::default())?);
332    /// let l2 = Arc::new(L2Cache::new().await?);
333    ///
334    /// let manager = CacheManager::new_with_backends(l1, l2, None)?;
335    /// # Ok(())
336    /// # }
337    /// ```
338    /// # Errors
339    ///
340    /// Returns `Ok` if successful. Currently no error conditions, but kept for future compatibility.
341    pub fn new_with_backends(
342        l1_cache: Arc<dyn CacheBackend>,
343        l2_cache: Arc<dyn L2CacheBackend>,
344        streaming_backend: Option<Arc<dyn StreamingBackend>>,
345    ) -> CacheResult<Self> {
346        debug!("Initializing Cache Manager with L1+L2 backends...");
347
348        let tiers = vec![
349            CacheTier::new(Arc::new(ProxyL1ToL2(l1_cache)), 1, false, 1, 1.0),
350            CacheTier::new(l2_cache, 2, true, 10, 1.0),
351        ];
352
353        Ok(Self {
354            tiers,
355            streaming_backend,
356            total_requests: AtomicU64::new(0),
357            l1_hits: AtomicU64::new(0),
358            l2_hits: AtomicU64::new(0),
359            misses: AtomicU64::new(0),
360            promotions: AtomicUsize::new(0),
361            in_flight_requests: Arc::new(DashMap::new()),
362            serializer: Arc::new(CacheSerializer::Json(JsonSerializer)),
363            #[cfg(feature = "redis")]
364            invalidation_publisher: None,
365            #[cfg(feature = "redis")]
366            invalidation_subscriber: None,
367            #[cfg(feature = "redis")]
368            invalidation_stats: Arc::new(AtomicInvalidationStats::default()),
369            instance_id: uuid::Uuid::new_v4().to_string(),
370        })
371    }
372
373    /// Create new cache manager with default backends (backward compatible)
374    ///
375    /// This is the legacy constructor maintained for backward compatibility.
376    /// New code should prefer `new_with_backends()` or `CacheSystemBuilder`.
377    ///
378    /// # Arguments
379    ///
380    /// * `l1_cache` - Moka L1 cache instance
381    /// * `l2_cache` - Redis L2 cache instance
382    /// # Errors
383    ///
384    /// Returns an error if Redis connection fails.
385    #[cfg(all(feature = "moka", feature = "redis"))]
386    #[cfg_attr(docsrs, doc(cfg(all(feature = "moka", feature = "redis"))))]
387    pub async fn new(l1_cache: Arc<L1Cache>, l2_cache: Arc<L2Cache>) -> CacheResult<Self> {
388        debug!("Initializing Cache Manager...");
389
390        // Create RedisStreams backend for streaming functionality
391        let streaming_backend: Option<Arc<dyn StreamingBackend>> = {
392            let redis_url =
393                std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
394            let redis_streams = crate::redis_streams::RedisStreams::new(&redis_url).await?;
395            Some(Arc::new(redis_streams))
396        };
397
398        let tiers = vec![
399            CacheTier::new(l1_cache as Arc<dyn L2CacheBackend>, 1, false, 1, 1.0),
400            CacheTier::new(l2_cache as Arc<dyn L2CacheBackend>, 2, true, 10, 1.0),
401        ];
402
403        Ok(Self {
404            tiers,
405            streaming_backend,
406            total_requests: AtomicU64::new(0),
407            l1_hits: AtomicU64::new(0),
408            l2_hits: AtomicU64::new(0),
409            misses: AtomicU64::new(0),
410            promotions: AtomicUsize::new(0),
411            in_flight_requests: Arc::new(DashMap::new()),
412            serializer: Arc::new(CacheSerializer::Json(JsonSerializer)),
413            #[cfg(feature = "redis")]
414            invalidation_publisher: None,
415            #[cfg(feature = "redis")]
416            invalidation_subscriber: None,
417            #[cfg(feature = "redis")]
418            invalidation_stats: Arc::new(AtomicInvalidationStats::default()),
419            instance_id: uuid::Uuid::new_v4().to_string(),
420        })
421    }
422
423    /// Create new cache manager with invalidation support
424    ///
425    /// This constructor enables cross-instance cache invalidation via Redis Pub/Sub.
426    ///
427    /// # Arguments
428    ///
429    /// * `l1_cache` - Moka L1 cache instance
430    /// * `l2_cache` - Redis L2 cache instance
431    /// * `redis_url` - Redis connection URL for Pub/Sub
432    /// * `config` - Invalidation configuration
433    ///
434    /// # Example
435    ///
436    /// ```rust,no_run
437    /// # #[tokio::main]
438    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
439    /// use multi_tier_cache::{CacheManager, L1Cache, L2Cache, InvalidationConfig, MokaCacheConfig};
440    /// use std::sync::Arc;
441    ///
442    /// let l1 = Arc::new(L1Cache::new(MokaCacheConfig::default())?);
443    /// let l2 = Arc::new(L2Cache::new().await?);
444    ///
445    /// let config = InvalidationConfig {
446    ///     channel: "my_app:cache:invalidate".to_string(),
447    ///     ..Default::default()
448    /// };
449    ///
450    /// let manager = CacheManager::new_with_invalidation(
451    ///     l1, l2, "redis://localhost", config
452    /// ).await?;
453    /// # Ok(())
454    /// # }
455    /// ```
456    /// # Errors
457    ///
458    /// Returns an error if Redis connection fails or invalidation setup fails.
459    #[cfg(all(feature = "moka", feature = "redis"))]
460    #[cfg_attr(docsrs, doc(cfg(all(feature = "moka", feature = "redis"))))]
461    pub async fn new_with_invalidation(
462        l1_cache: Arc<L1Cache>,
463        l2_cache: Arc<L2Cache>,
464        redis_url: &str,
465        config: InvalidationConfig,
466    ) -> CacheResult<Self> {
467        debug!("Initializing Cache Manager with Invalidation...");
468        debug!("  Pub/Sub channel: {}", config.channel);
469
470        // Create RedisStreams backend for streaming functionality
471        let streaming_backend: Option<Arc<dyn StreamingBackend>> = {
472            let redis_streams = crate::redis_streams::RedisStreams::new(redis_url).await?;
473            Some(Arc::new(redis_streams))
474        };
475
476        let instance_id = uuid::Uuid::new_v4().to_string();
477
478        // Create publisher & subscriber
479        let (invalidation_publisher, invalidation_subscriber) = {
480            let client = redis::Client::open(redis_url)?;
481            let conn_manager = redis::aio::ConnectionManager::new(client).await?;
482            let publisher = InvalidationPublisher::new(conn_manager, config.clone());
483
484            // Create subscriber with instance ID to avoid self-invalidation echo
485            let subscriber = InvalidationSubscriber::new(redis_url, config.clone())?
486                .with_instance_id(&instance_id);
487            (
488                Some(Arc::new(publisher)),
489                Some(Arc::new(subscriber)),
490            )
491        };
492
493        let invalidation_stats = Arc::new(AtomicInvalidationStats::default());
494
495        let tiers = vec![
496            CacheTier::new(l1_cache as Arc<dyn L2CacheBackend>, 1, false, 1, 1.0),
497            CacheTier::new(l2_cache as Arc<dyn L2CacheBackend>, 2, true, 10, 1.0),
498        ];
499
500        let manager = Self {
501            tiers,
502            streaming_backend,
503            total_requests: AtomicU64::new(0),
504            l1_hits: AtomicU64::new(0),
505            l2_hits: AtomicU64::new(0),
506            misses: AtomicU64::new(0),
507            promotions: AtomicUsize::new(0),
508            in_flight_requests: Arc::new(DashMap::new()),
509            serializer: Arc::new(CacheSerializer::Json(JsonSerializer)),
510            invalidation_publisher,
511            invalidation_subscriber,
512            invalidation_stats,
513            instance_id,
514        };
515
516        // Start subscriber with handler
517        manager.start_invalidation_subscriber();
518
519        info!("Cache Manager initialized with invalidation support");
520
521        Ok(manager)
522    }
523
524    /// Create new cache manager with multi-tier architecture (v0.5.0+)
525    ///
526    /// This constructor enables dynamic multi-tier caching with 3, 4, or more tiers.
527    /// Tiers are checked in order (lower `tier_level` = faster/hotter).
528    ///
529    /// # Arguments
530    ///
531    /// * `tiers` - Vector of configured cache tiers (must be sorted by `tier_level` ascending)
532    /// * `streaming_backend` - Optional streaming backend
533    ///
534    /// # Example
535    ///
536    /// ```rust,no_run
537    /// # #[tokio::main]
538    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
539    /// use multi_tier_cache::{CacheManager, CacheTier, TierConfig, L1Cache, L2Cache, MokaCacheConfig};
540    /// use std::sync::Arc;
541    ///
542    /// // L1 + L2 setup
543    /// let l1 = Arc::new(L1Cache::new(MokaCacheConfig::default())?);
544    /// let l2 = Arc::new(L2Cache::new().await?);
545    ///
546    /// let tiers = vec![
547    ///     CacheTier::new(l1, 1, false, 1, 1.0),  // L1 - no promotion
548    ///     CacheTier::new(l2, 2, true, 10, 1.0),   // L2 - promote to L1
549    /// ];
550    ///
551    /// let manager = CacheManager::new_with_tiers(tiers, None)?;
552    /// # Ok(())
553    /// # }
554    /// ```
555    /// # Errors
556    ///
557    /// Returns an error if tiers are not sorted by level or if no tiers are provided.
558    pub fn new_with_tiers(
559        tiers: Vec<CacheTier>,
560        streaming_backend: Option<Arc<dyn StreamingBackend>>,
561    ) -> CacheResult<Self> {
562        debug!("Initializing Multi-Tier Cache Manager...");
563        debug!("  Tier count: {}", tiers.len());
564        for tier in &tiers {
565            debug!(
566                "  L{}: {} (promotion={}, ttl_scale={})",
567                tier.tier_level, tier.stats.backend_name, tier.promotion_enabled, tier.ttl_scale
568            );
569        }
570
571        // Validate tiers are sorted by level
572        for i in 1..tiers.len() {
573            if let (Some(current), Some(prev)) = (tiers.get(i), tiers.get(i - 1))
574                && current.tier_level <= prev.tier_level
575            {
576                return Err(crate::error::CacheError::ConfigError(format!(
577                    "Tiers must be sorted by tier_level ascending (found L{} after L{})",
578                    current.tier_level, prev.tier_level
579                )));
580            }
581        }
582
583        Ok(Self {
584            tiers,
585            streaming_backend,
586            total_requests: AtomicU64::new(0),
587            l1_hits: AtomicU64::new(0),
588            l2_hits: AtomicU64::new(0),
589            misses: AtomicU64::new(0),
590            promotions: AtomicUsize::new(0),
591            in_flight_requests: Arc::new(DashMap::new()),
592            serializer: Arc::new(CacheSerializer::Json(JsonSerializer)),
593            #[cfg(feature = "redis")]
594            invalidation_publisher: None,
595            #[cfg(feature = "redis")]
596            invalidation_subscriber: None,
597            #[cfg(feature = "redis")]
598            invalidation_stats: Arc::new(AtomicInvalidationStats::default()),
599            instance_id: uuid::Uuid::new_v4().to_string(),
600        })
601    }
602
603    /// Set a custom serializer for the cache manager
604    pub fn set_serializer(&mut self, serializer: CacheSerializer) {
605        debug!(name = %serializer.name(), "Switching cache serializer");
606        self.serializer = Arc::new(serializer);
607    }
608
609    /// Start the invalidation subscriber background task
610    #[cfg(feature = "redis")]
611    #[cfg_attr(docsrs, doc(cfg(feature = "redis")))]
612    fn start_invalidation_subscriber(&self) {
613        #[cfg(feature = "redis")]
614        if let Some(subscriber) = &self.invalidation_subscriber {
615            let tiers = self.tiers.clone();
616
617            subscriber.start(move |msg: crate::invalidation::InvalidationMessage| {
618                let tiers = tiers.clone();
619                async move {
620                    for tier in &tiers {
621                        match &msg {
622                            InvalidationMessage::Remove { key } => {
623                                tier.backend.remove(key).await.ok();
624                            }
625                            InvalidationMessage::Update {
626                                key,
627                                value,
628                                ttl_secs,
629                            } => {
630                                if let Some(secs) = ttl_secs {
631                                    tier.backend
632                                        .set_with_ttl(
633                                            key,
634                                            value.clone(),
635                                            Duration::from_secs(*secs),
636                                        )
637                                        .await
638                                        .ok();
639                                } else {
640                                    tier.backend.set(key, value.clone()).await.ok();
641                                }
642                            }
643                            InvalidationMessage::RemovePattern { pattern } => {
644                                if let Err(e) = tier.backend.remove_pattern(pattern).await {
645                                    warn!(
646                                        "Failed to remove pattern '{}' from L{}: {}",
647                                        pattern, tier.tier_level, e
648                                    );
649                                }
650                            }
651                            InvalidationMessage::RemoveBulk { keys } => {
652                                for key in keys {
653                                    if let Err(e) = tier.backend.remove(key).await {
654                                        warn!(
655                                            "Failed to remove '{}' from L{}: {}",
656                                            key, tier.tier_level, e
657                                        );
658                                    }
659                                }
660                            }
661                        }
662                    }
663                    Ok(())
664                }
665            });
666
667            info!("Invalidation subscriber started across all tiers");
668        }
669    }
670
671    /// Get value from cache using multi-tier architecture (v0.5.0+)
672    ///
673    /// This method iterates through all configured tiers and automatically promotes
674    /// to upper tiers on cache hit.
675    async fn get_multi_tier(&self, key: &str) -> CacheResult<Option<Bytes>> {
676        self.get_multi_tier_from(key, 0).await
677    }
678
679    /// Get value from cache starting from a specific tier index
680    async fn get_multi_tier_from(
681        &self,
682        key: &str,
683        start_index: usize,
684    ) -> CacheResult<Option<Bytes>> {
685        // Try each tier sequentially (sorted by tier_level) starting from start_index
686        for (tier_index, tier) in self.tiers.iter().enumerate().skip(start_index) {
687            if let Some((value, ttl)) = tier.get_with_ttl(key).await {
688                // Cache hit!
689                tier.record_hit();
690                if tier.tier_level == 1 {
691                    self.l1_hits.fetch_add(1, Ordering::Relaxed);
692                } else if tier.tier_level == 2 {
693                    self.l2_hits.fetch_add(1, Ordering::Relaxed);
694                }
695
696                // Promote to all upper tiers (if promotion enabled)
697                if tier.promotion_enabled && tier_index > 0 {
698                    // Probabilistic Promotion Check
699                    let should_promote = if tier.promotion_frequency <= 1 {
700                        true
701                    } else {
702                        rand::thread_rng().gen_ratio(
703                            1,
704                            u32::try_from(tier.promotion_frequency).unwrap_or(u32::MAX),
705                        )
706                    };
707
708                    if should_promote {
709                        let promotion_ttl =
710                            ttl.unwrap_or_else(|| CacheStrategy::Default.to_duration());
711
712                        // Promote to all tiers above this one
713                        for upper_tier in self.tiers.iter().take(tier_index).rev() {
714                            if let Err(e) = upper_tier
715                                .set_with_ttl(key, value.clone(), promotion_ttl)
716                                .await
717                            {
718                                warn!(
719                                    "Failed to promote '{}' from L{} to L{}: {}",
720                                    key, tier.tier_level, upper_tier.tier_level, e
721                                );
722                            } else {
723                                self.promotions.fetch_add(1, Ordering::Relaxed);
724                                debug!(
725                                    "Promoted '{}' from L{} to L{} (TTL: {:?})",
726                                    key, tier.tier_level, upper_tier.tier_level, promotion_ttl
727                                );
728                            }
729                        }
730                    } else {
731                        debug!(
732                            "Probabilistic skip promotion for '{}' from L{} (N={})",
733                            key, tier.tier_level, tier.promotion_frequency
734                        );
735                    }
736                }
737
738                return Ok(Some(value));
739            }
740        }
741
742        // Cache miss across all tiers
743        Ok(None)
744    }
745
746    /// Get value from cache (L1 first, then L2 fallback with promotion)
747    ///
748    /// This method now includes built-in Cache Stampede protection when cache misses occur.
749    /// Multiple concurrent requests for the same missing key will be coalesced to prevent
750    /// unnecessary duplicate work on external data sources.
751    ///
752    /// Supports both legacy 2-tier mode and new multi-tier mode (v0.5.0+).
753    ///
754    /// # Arguments
755    /// * `key` - Cache key to retrieve
756    ///
757    /// # Returns
758    /// * `Ok(Some(value))` - Cache hit, value found in any tier
759    /// * `Ok(None)` - Cache miss, value not found in any cache
760    /// * `Err(error)` - Cache operation failed
761    /// # Errors
762    ///
763    /// Returns an error if cache operation fails.
764    ///
765    /// # Panics
766    ///
767    /// Panics if tiers are not initialized in multi-tier mode (should not happen if constructed correctly).
768    pub async fn get(&self, key: &str) -> CacheResult<Option<Bytes>> {
769        self.total_requests.fetch_add(1, Ordering::Relaxed);
770
771        // Fast path for L1 (first tier) - no locking needed
772        if let Some(tier1) = self.tiers.first()
773            && let Some((value, _ttl)) = tier1.get_with_ttl(key).await
774        {
775            tier1.record_hit();
776            // Update legacy stats for backward compatibility
777            self.l1_hits.fetch_add(1, Ordering::Relaxed);
778            return Ok(Some(value));
779        }
780
781        let key_owned = key.to_string();
782        let flight_state = match self.in_flight_requests.entry(key_owned.clone()) {
783            dashmap::mapref::entry::Entry::Occupied(entry) => {
784                FlightState::Waiter(entry.get().subscribe())
785            }
786            dashmap::mapref::entry::Entry::Vacant(entry) => {
787                let (tx, _) = watch::channel(None);
788                let tx = Arc::new(tx);
789                entry.insert(tx.clone());
790                FlightState::Creator(tx)
791            }
792        };
793
794        match flight_state {
795            FlightState::Waiter(mut rx) => {
796                // Wait for creator to finish
797                if rx.borrow().is_none() {
798                    while rx.changed().await.is_ok() {
799                        if rx.borrow().is_some() {
800                            break;
801                        }
802                    }
803                }
804                // Return result if it exists, otherwise fall through to re-compute
805                if let Some(res) = rx.borrow().clone() {
806                    return res;
807                }
808            }
809            FlightState::Creator(tx) => {
810                // Creator - guard key removal on drop/cancellation
811                let _guard = RemoveInFlightGuard {
812                    map: Arc::clone(&self.in_flight_requests),
813                    key: key_owned,
814                };
815
816                // Double-check L1 after acquiring lock (or if we are the first to compute)
817                if let Some(tier1) = self.tiers.first()
818                    && let Some((value, _ttl)) = tier1.get_with_ttl(key).await
819                {
820                    tier1.record_hit();
821                    self.l1_hits.fetch_add(1, Ordering::Relaxed);
822                    let _ = tx.send(Some(Ok(Some(value.clone())))); // Notify any waiting subscribers
823                    return Ok(Some(value));
824                }
825
826                // Check remaining tiers with promotion (start from tier index 1: L2)
827                let result = self.get_multi_tier_from(key, 1).await;
828
829                match &result {
830                    Ok(Some(val)) => {
831                        let _ = tx.send(Some(Ok(Some(val.clone()))));
832                    }
833                    Ok(None) => {
834                        self.misses.fetch_add(1, Ordering::Relaxed);
835                        let _ = tx.send(Some(Ok(None)));
836                    }
837                    Err(e) => {
838                        let _ = tx.send(Some(Err(e.clone())));
839                    }
840                }
841
842                return result;
843            }
844        }
845
846        // If waiter fell through (creator dropped without sending), do direct fallback query
847        let result = self.get_multi_tier_from(key, 1).await;
848        if let Ok(None) = result {
849            self.misses.fetch_add(1, Ordering::Relaxed);
850        }
851        result
852    }
853
854    /// Get a value from cache and deserialize it (Type-Safe Version)
855    ///
856    /// # Errors
857    ///
858    /// Returns a `SerializationError` if deserialization fails, or a `BackendError` if the cache retrieval fails.
859    pub async fn get_typed<T>(&self, key: &str) -> CacheResult<Option<T>>
860    where
861        T: serde::de::DeserializeOwned,
862    {
863        if let Some(bytes) = self.get(key).await? {
864            return Ok(Some(self.serializer.deserialize::<T>(&bytes)?));
865        }
866        Ok(None)
867    }
868
869    /// Set value with specific cache strategy (all tiers)
870    ///
871    /// Supports both legacy 2-tier mode and new multi-tier mode (v0.5.0+).
872    /// In multi-tier mode, stores to ALL tiers with their respective TTL scaling.
873    /// # Errors
874    ///
875    /// Returns an error if cache set operation fails.
876    pub async fn set_with_strategy(
877        &self,
878        key: &str,
879        value: Bytes,
880        strategy: CacheStrategy,
881    ) -> CacheResult<()> {
882        let ttl = strategy.to_duration();
883
884        let mut success_count = 0;
885        let mut last_error = None;
886
887        for tier in &self.tiers {
888            match tier.set_with_ttl(key, value.clone(), ttl).await {
889                Ok(()) => {
890                    success_count += 1;
891                }
892                Err(e) => {
893                    error!(
894                        "L{} cache set failed for key '{}': {}",
895                        tier.tier_level, key, e
896                    );
897                    last_error = Some(e);
898                }
899            }
900        }
901
902        if success_count > 0 {
903            debug!(
904                "[Cache] Stored '{}' in {}/{} tiers (base TTL: {:?})",
905                key,
906                success_count,
907                self.tiers.len(),
908                ttl
909            );
910            return Ok(());
911        }
912
913        Err(last_error.unwrap_or_else(|| {
914            crate::error::CacheError::InternalError("All tiers failed".to_string())
915        }))
916    }
917
918    /// Get or compute value with Cache Stampede protection across L1+L2+Compute
919    ///
920    /// This method provides comprehensive Cache Stampede protection:
921    /// 1. Check L1 cache first (uses Moka's built-in coalescing)
922    /// 2. Check L2 cache with mutex-based coalescing
923    /// 3. Compute fresh data with protection against concurrent computations
924    ///
925    /// # Arguments
926    /// * `key` - Cache key
927    /// * `strategy` - Cache strategy for TTL and storage behavior
928    /// * `compute_fn` - Async function to compute the value if not in any cache
929    ///
930    /// # Example
931    /// ```rust,no_run
932    /// # #[tokio::main]
933    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
934    /// # use multi_tier_cache::{CacheManager, CacheStrategy, L1Cache, L2Cache, MokaCacheConfig};
935    /// # use std::sync::Arc;
936    /// # let l1 = Arc::new(L1Cache::new(MokaCacheConfig::default())?);
937    /// # let l2 = Arc::new(L2Cache::new().await?);
938    /// # let cache_manager = CacheManager::new(l1, l2).await?;
939    /// # async fn fetch_data_from_api() -> Result<bytes::Bytes, multi_tier_cache::error::CacheError> { Ok(bytes::Bytes::new()) }
940    /// let api_data = cache_manager.get_or_compute_with(
941    ///     "api_response",
942    ///     CacheStrategy::RealTime,
943    ///     || async {
944    ///         fetch_data_from_api().await
945    ///     }
946    /// ).await?;
947    /// # Ok(())
948    /// # }
949    /// ```
950    #[allow(dead_code)]
951    /// # Errors
952    ///
953    /// Returns an error if compute function fails or cache operations fail.
954    pub async fn get_or_compute_with<F, Fut>(
955        &self,
956        key: &str,
957        strategy: CacheStrategy,
958        compute_fn: F,
959    ) -> CacheResult<Bytes>
960    where
961        F: FnOnce() -> Fut + Send,
962        Fut: Future<Output = CacheResult<Bytes>> + Send,
963    {
964        self.total_requests.fetch_add(1, Ordering::Relaxed);
965
966        // 1. Try tiers sequentially first
967        if let Some(value) = self.get_multi_tier(key).await? {
968            return Ok(value);
969        }
970
971        let key_owned = key.to_string();
972        let flight_state = match self.in_flight_requests.entry(key_owned.clone()) {
973            dashmap::mapref::entry::Entry::Occupied(entry) => {
974                FlightState::Waiter(entry.get().subscribe())
975            }
976            dashmap::mapref::entry::Entry::Vacant(entry) => {
977                let (tx, _) = watch::channel(None);
978                let tx = Arc::new(tx);
979                entry.insert(tx.clone());
980                FlightState::Creator(tx)
981            }
982        };
983
984        match flight_state {
985            FlightState::Waiter(mut rx) => {
986                // Wait for creator to finish
987                if rx.borrow().is_none() {
988                    while rx.changed().await.is_ok() {
989                        if rx.borrow().is_some() {
990                            break;
991                        }
992                    }
993                }
994                // Return result if it exists, otherwise fall through to re-compute
995                if let Some(res) = rx.borrow().clone() {
996                    match res {
997                        Ok(Some(bytes)) => return Ok(bytes),
998                        Ok(None) => {} // Miss, fall through to re-compute
999                        Err(e) => return Err(e),
1000                    }
1001                }
1002            }
1003            FlightState::Creator(tx) => {
1004                // Guard key removal on drop/cancellation
1005                let _guard = RemoveInFlightGuard {
1006                    map: Arc::clone(&self.in_flight_requests),
1007                    key: key_owned,
1008                };
1009
1010                // 3. Re-check cache after receiving/creating broadcaster (double-check pattern)
1011                if let Some(value) = self.get_multi_tier(key).await? {
1012                    let _ = tx.send(Some(Ok(Some(value.clone()))));
1013                    return Ok(value);
1014                }
1015
1016                // 4. Miss - compute fresh data
1017                debug!(
1018                    "Computing fresh data for key: '{}' (Stampede protected)",
1019                    key
1020                );
1021
1022                let result = compute_fn().await;
1023
1024                match &result {
1025                    Ok(value) => {
1026                        let _ = self.set_with_strategy(key, value.clone(), strategy).await;
1027                        let _ = tx.send(Some(Ok(Some(value.clone()))));
1028                    }
1029                    Err(e) => {
1030                        let _ = tx.send(Some(Err(e.clone())));
1031                    }
1032                }
1033
1034                return result;
1035            }
1036        }
1037
1038        // If waiter fell through (creator dropped or miss without sending), do direct compute
1039        debug!(
1040            "Computing fresh data for key: '{}' (Stampede fallback)",
1041            key
1042        );
1043        let result = compute_fn().await;
1044        if let Ok(value) = &result {
1045            let _ = self.set_with_strategy(key, value.clone(), strategy).await;
1046        }
1047        result
1048    }
1049
1050    /// Get or compute typed value with Cache Stampede protection (Type-Safe Version)
1051    ///
1052    /// This method provides the same functionality as `get_or_compute_with()` but with
1053    /// **type-safe** automatic serialization/deserialization. Perfect for database queries,
1054    /// API calls, or any computation that returns structured data.
1055    ///
1056    /// # Type Safety
1057    ///
1058    /// - Returns your actual type `T` instead of `serde_json::Value`
1059    /// - Compiler enforces Serialize + `DeserializeOwned` bounds
1060    /// - No manual JSON conversion needed
1061    ///
1062    /// # Cache Flow
1063    ///
1064    /// 1. Check L1 cache → deserialize if found
1065    /// 2. Check L2 cache → deserialize + promote to L1 if found
1066    /// 3. Execute `compute_fn` → serialize → store in L1+L2
1067    /// 4. Full stampede protection (only ONE request computes)
1068    ///
1069    /// # Arguments
1070    ///
1071    /// * `key` - Cache key
1072    /// * `strategy` - Cache strategy for TTL
1073    /// * `compute_fn` - Async function returning `Result<T>`
1074    ///
1075    /// # Example - Database Query
1076    ///
1077    /// ```no_run
1078    /// # use multi_tier_cache::{CacheManager, CacheStrategy, L1Cache, L2Cache, MokaCacheConfig};
1079    /// # use std::sync::Arc;
1080    /// # use serde::{Serialize, Deserialize};
1081    /// # async fn example() -> anyhow::Result<()> {
1082    /// # let l1 = Arc::new(L1Cache::new(MokaCacheConfig::default())?);
1083    /// # let l2 = Arc::new(L2Cache::new().await?);
1084    /// # let cache_manager = CacheManager::new(l1, l2);
1085    ///
1086    /// #[derive(Serialize, Deserialize)]
1087    /// struct User {
1088    ///     id: i64,
1089    ///     name: String,
1090    /// }
1091    ///
1092    /// // Type-safe database caching (example - requires sqlx)
1093    /// // let user: User = cache_manager.get_or_compute_typed(
1094    /// //     "user:123",
1095    /// //     CacheStrategy::MediumTerm,
1096    /// //     || async {
1097    /// //         sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
1098    /// //             .bind(123)
1099    /// //             .fetch_one(&pool)
1100    /// //             .await
1101    /// //     }
1102    /// // ).await?;
1103    /// # Ok(())
1104    /// # }
1105    /// ```
1106    ///
1107    /// # Example - API Call
1108    ///
1109    /// ```no_run
1110    /// # use multi_tier_cache::{CacheManager, CacheStrategy, L1Cache, L2Cache, MokaCacheConfig};
1111    /// # use std::sync::Arc;
1112    /// # use serde::{Serialize, Deserialize};
1113    /// # async fn example() -> anyhow::Result<()> {
1114    /// # let l1 = Arc::new(L1Cache::new(MokaCacheConfig::default())?);
1115    /// # let l2 = Arc::new(L2Cache::new().await?);
1116    /// # let cache_manager = CacheManager::new(l1, l2);
1117    /// #[derive(Serialize, Deserialize)]
1118    /// struct ApiResponse {
1119    ///     data: String,
1120    ///     timestamp: i64,
1121    /// }
1122    ///
1123    /// // API call caching (example - requires reqwest)
1124    /// // let response: ApiResponse = cache_manager.get_or_compute_typed(
1125    /// //     "api:endpoint",
1126    /// //     CacheStrategy::RealTime,
1127    /// //     || async {
1128    /// //         reqwest::get("https://api.example.com/data")
1129    /// //             .await?
1130    /// //             .json::<ApiResponse>()
1131    /// //             .await
1132    /// //     }
1133    /// // ).await?;
1134    /// # Ok(())
1135    /// # }
1136    /// ```
1137    ///
1138    /// # Performance
1139    ///
1140    /// - L1 hit: <1ms + deserialization (~10-50μs for small structs)
1141    /// - L2 hit: 2-5ms + deserialization + L1 promotion
1142    /// - Compute: Your function time + serialization + L1+L2 storage
1143    /// - Stampede protection: 99.6% latency reduction under high concurrency
1144    ///
1145    /// # Errors
1146    ///
1147    /// Returns error if:
1148    /// - Compute function fails
1149    /// - Serialization fails (invalid type for JSON)
1150    /// - Deserialization fails (cache data doesn't match type T)
1151    /// - Cache operations fail (Redis connection issues)
1152    #[allow(clippy::too_many_lines)]
1153    pub async fn get_or_compute_typed<T, F, Fut>(
1154        &self,
1155        key: &str,
1156        strategy: CacheStrategy,
1157        compute_fn: F,
1158    ) -> CacheResult<T>
1159    where
1160        T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static,
1161        F: FnOnce() -> Fut + Send,
1162        Fut: Future<Output = CacheResult<T>> + Send,
1163    {
1164        // 1. Try to get typed from cache first
1165        if let Some(value) = self.get_typed::<T>(key).await? {
1166            return Ok(value);
1167        }
1168
1169        // 2. Use get_or_compute_with to handle stampede protection
1170        let computed_val = Arc::new(std::sync::Mutex::new(None));
1171        let computed_val_clone = Arc::clone(&computed_val);
1172        let serializer = self.serializer.clone();
1173        let bytes_result = self
1174            .get_or_compute_with(key, strategy, || async move {
1175                let val = compute_fn().await?;
1176                let bytes = serializer.serialize(&val)?;
1177                if let Ok(mut guard) = computed_val_clone.lock() {
1178                    *guard = Some(val);
1179                }
1180                Ok(bytes)
1181            })
1182            .await?;
1183
1184        // 3. Fast-path: if this task computed the value, return it directly without deserializing
1185        if let Ok(mut guard) = computed_val.lock()
1186            && let Some(val) = guard.take()
1187        {
1188            return Ok(val);
1189        }
1190
1191        // 4. Fallback for waiters: deserialize from bytes
1192        self.serializer.deserialize::<T>(&bytes_result)
1193    }
1194
1195    /// Get comprehensive cache statistics
1196    ///
1197    /// In multi-tier mode, aggregates statistics from all tiers.
1198    /// In legacy mode, returns L1 and L2 stats.
1199    #[allow(dead_code)]
1200    pub fn get_stats(&self) -> CacheManagerStats {
1201        let total_reqs = self.total_requests.load(Ordering::Relaxed);
1202        let l1_hits = self.l1_hits.load(Ordering::Relaxed);
1203        let l2_hits = self.l2_hits.load(Ordering::Relaxed);
1204        let misses = self.misses.load(Ordering::Relaxed);
1205
1206        CacheManagerStats {
1207            total_requests: total_reqs,
1208            l1_hits,
1209            l2_hits,
1210            total_hits: l1_hits + l2_hits,
1211            misses,
1212            hit_rate: if total_reqs > 0 {
1213                #[allow(clippy::cast_precision_loss)]
1214                {
1215                    ((l1_hits + l2_hits) as f64 / total_reqs as f64) * 100.0
1216                }
1217            } else {
1218                0.0
1219            },
1220            l1_hit_rate: if total_reqs > 0 {
1221                #[allow(clippy::cast_precision_loss)]
1222                {
1223                    (l1_hits as f64 / total_reqs as f64) * 100.0
1224                }
1225            } else {
1226                0.0
1227            },
1228            promotions: self.promotions.load(Ordering::Relaxed),
1229            in_flight_requests: self.in_flight_requests.len(),
1230        }
1231    }
1232
1233    /// Get per-tier statistics (v0.5.0+)
1234    ///
1235    /// Returns statistics for each tier if multi-tier mode is enabled.
1236    /// Returns None if using legacy 2-tier mode.
1237    ///
1238    /// # Example
1239    /// ```rust,no_run
1240    /// # #[tokio::main]
1241    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1242    /// # use multi_tier_cache::{CacheManager, L1Cache, L2Cache, MokaCacheConfig};
1243    /// # use std::sync::Arc;
1244    /// # let l1 = Arc::new(L1Cache::new(MokaCacheConfig::default())?);
1245    /// # let l2 = Arc::new(L2Cache::new().await?);
1246    /// # let cache_manager = CacheManager::new(l1, l2).await?;
1247    /// let tier_stats = cache_manager.get_tier_stats();
1248    /// for stats in tier_stats {
1249    ///     println!("L{}: {} hits ({})",
1250    ///              stats.tier_level,
1251    ///              stats.hit_count(),
1252    ///              stats.backend_name);
1253    /// }
1254    /// # Ok(())
1255    /// # }
1256    /// ```
1257    pub fn get_tier_stats(&self) -> Vec<TierStats> {
1258        self.tiers.iter().map(|tier| tier.stats.clone()).collect()
1259    }
1260}
1261
1262/// Proxy wrapper to allow using `CacheBackend` where `DynL2CacheBackend` is expected
1263/// (Internal helper for `new_with_backends` to wrap L1 `CacheBackend` into `DynL2CacheBackend`)
1264struct ProxyL1ToL2(Arc<dyn CacheBackend>);
1265
1266impl CacheBackend for ProxyL1ToL2 {
1267    fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Option<Bytes>> {
1268        self.0.get(key)
1269    }
1270
1271    fn set_with_ttl<'a>(
1272        &'a self,
1273        key: &'a str,
1274        value: Bytes,
1275        ttl: Duration,
1276    ) -> BoxFuture<'a, CacheResult<()>> {
1277        self.0.set_with_ttl(key, value, ttl)
1278    }
1279
1280    fn remove<'a>(&'a self, key: &'a str) -> BoxFuture<'a, CacheResult<()>> {
1281        self.0.remove(key)
1282    }
1283
1284    fn remove_pattern<'a>(&'a self, pattern: &'a str) -> BoxFuture<'a, CacheResult<()>> {
1285        self.0.remove_pattern(pattern)
1286    }
1287
1288    fn health_check(&self) -> BoxFuture<'_, bool> {
1289        self.0.health_check()
1290    }
1291
1292    fn name(&self) -> &'static str {
1293        self.0.name()
1294    }
1295}
1296
1297impl L2CacheBackend for ProxyL1ToL2 {
1298    fn get_with_ttl<'a>(
1299        &'a self,
1300        key: &'a str,
1301    ) -> BoxFuture<'a, Option<(Bytes, Option<Duration>)>> {
1302        Box::pin(async move { self.0.get(key).await.map(|v| (v, None)) })
1303    }
1304}
1305
1306impl CacheManager {
1307    // ===== Redis Streams Methods =====
1308
1309    /// Publish data to Redis Stream
1310    ///
1311    /// # Arguments
1312    /// * `stream_key` - Name of the stream (e.g., "`events_stream`")
1313    /// * `fields` - Field-value pairs to publish
1314    /// * `maxlen` - Optional max length for stream trimming
1315    ///
1316    /// # Returns
1317    /// The entry ID generated by Redis
1318    ///
1319    /// # Errors
1320    /// Returns error if streaming backend is not configured
1321    pub async fn publish_to_stream(
1322        &self,
1323        stream_key: &str,
1324        fields: Vec<(String, String)>,
1325        maxlen: Option<usize>,
1326    ) -> CacheResult<String> {
1327        match &self.streaming_backend {
1328            Some(backend) => backend.stream_add(stream_key, fields, maxlen).await,
1329            None => Err(crate::error::CacheError::ConfigError(
1330                "Streaming backend not configured".to_string(),
1331            )),
1332        }
1333    }
1334
1335    /// Read latest entries from Redis Stream
1336    ///
1337    /// # Arguments
1338    /// * `stream_key` - Name of the stream
1339    /// * `count` - Number of latest entries to retrieve
1340    ///
1341    /// # Returns
1342    /// Vector of (`entry_id`, fields) tuples (newest first)
1343    ///
1344    /// # Errors
1345    /// Returns error if streaming backend is not configured
1346    pub async fn read_stream_latest(
1347        &self,
1348        stream_key: &str,
1349        count: usize,
1350    ) -> CacheResult<Vec<(String, Vec<(String, String)>)>> {
1351        match &self.streaming_backend {
1352            Some(backend) => backend.stream_read_latest(stream_key, count).await,
1353            None => Err(crate::error::CacheError::ConfigError(
1354                "Streaming backend not configured".to_string(),
1355            )),
1356        }
1357    }
1358
1359    /// Read from Redis Stream with optional blocking
1360    ///
1361    /// # Arguments
1362    /// * `stream_key` - Name of the stream
1363    /// * `last_id` - Last ID seen ("0" for start, "$" for new only)
1364    /// * `count` - Max entries to retrieve
1365    /// * `block_ms` - Optional blocking timeout in ms
1366    ///
1367    /// # Returns
1368    /// Vector of (`entry_id`, fields) tuples
1369    ///
1370    /// # Errors
1371    /// Returns error if streaming backend is not configured
1372    pub async fn read_stream(
1373        &self,
1374        stream_key: &str,
1375        last_id: &str,
1376        count: usize,
1377        block_ms: Option<usize>,
1378    ) -> CacheResult<Vec<(String, Vec<(String, String)>)>> {
1379        match &self.streaming_backend {
1380            Some(backend) => {
1381                backend
1382                    .stream_read(stream_key, last_id, count, block_ms)
1383                    .await
1384            }
1385            None => Err(crate::error::CacheError::ConfigError(
1386                "Streaming backend not configured".to_string(),
1387            )),
1388        }
1389    }
1390
1391    // ===== Cache Invalidation Methods =====
1392
1393    /// Invalidate a cache key across all instances
1394    ///
1395    /// This removes the key from all cache tiers and broadcasts
1396    /// the invalidation to all other cache instances via Redis Pub/Sub.
1397    ///
1398    /// Supports both legacy 2-tier mode and new multi-tier mode (v0.5.0+).
1399    ///
1400    /// # Arguments
1401    /// * `key` - Cache key to invalidate
1402    ///
1403    /// # Example
1404    /// ```rust,no_run
1405    /// # use multi_tier_cache::CacheManager;
1406    /// # async fn example(cache_manager: &CacheManager) -> anyhow::Result<()> {
1407    /// // Invalidate user cache after profile update
1408    /// cache_manager.invalidate("user:123").await?;
1409    /// # Ok(())
1410    /// # }
1411    /// ```
1412    /// # Errors
1413    ///
1414    /// Returns an error if invalidation fails.
1415    pub async fn invalidate(&self, key: &str) -> CacheResult<()> {
1416        // Remove from ALL tiers
1417        for tier in &self.tiers {
1418            if let Err(e) = tier.remove(key).await {
1419                warn!(
1420                    "Failed to remove '{}' from L{}: {}",
1421                    key, tier.tier_level, e
1422                );
1423            }
1424        }
1425
1426        // Broadcast to other instances
1427        #[cfg(feature = "redis")]
1428        {
1429            if let Some(publisher) = &self.invalidation_publisher {
1430                let msg = InvalidationMessage::remove(key);
1431                publisher.publish_with_origin(&msg, Some(&self.instance_id)).await?;
1432                self.invalidation_stats
1433                    .messages_sent
1434                    .fetch_add(1, Ordering::Relaxed);
1435            }
1436        }
1437
1438        debug!("Invalidated '{}' across all instances", key);
1439        Ok(())
1440    }
1441
1442    /// Update cache value across all instances
1443    ///
1444    /// This updates the key in all cache tiers and broadcasts
1445    /// the update to all other cache instances, avoiding cache misses.
1446    ///
1447    /// Supports both legacy 2-tier mode and new multi-tier mode (v0.5.0+).
1448    ///
1449    /// # Arguments
1450    /// * `key` - Cache key to update
1451    /// * `value` - New value
1452    /// * `ttl` - Optional TTL (uses default if None)
1453    ///
1454    /// # Example
1455    /// ```rust,no_run
1456    /// # use multi_tier_cache::CacheManager;
1457    /// # use std::time::Duration;
1458    /// # use bytes::Bytes;
1459    /// # async fn example(cache_manager: &CacheManager) -> anyhow::Result<()> {
1460    /// // Update user cache with new data
1461    /// let user_data = Bytes::from("alice");
1462    /// cache_manager.update_cache("user:123", user_data, Some(Duration::from_secs(3600))).await?;
1463    /// # Ok(())
1464    /// # }
1465    /// ```
1466    /// # Errors
1467    ///
1468    /// Returns an error if cache update fails.
1469    pub async fn update_cache(
1470        &self,
1471        key: &str,
1472        value: Bytes,
1473        ttl: Option<Duration>,
1474    ) -> CacheResult<()> {
1475        let ttl = ttl.unwrap_or_else(|| CacheStrategy::Default.to_duration());
1476
1477        // Update ALL tiers
1478        for tier in &self.tiers {
1479            if let Err(e) = tier.set_with_ttl(key, value.clone(), ttl).await {
1480                warn!("Failed to update '{}' in L{}: {}", key, tier.tier_level, e);
1481            }
1482        }
1483
1484        // Broadcast update to other instances
1485        #[cfg(feature = "redis")]
1486        if let Some(publisher) = &self.invalidation_publisher {
1487            let msg = InvalidationMessage::update(key, value, Some(ttl));
1488            publisher.publish_with_origin(&msg, Some(&self.instance_id)).await?;
1489            self.invalidation_stats
1490                .messages_sent
1491                .fetch_add(1, Ordering::Relaxed);
1492        }
1493
1494        debug!("Updated '{}' across all instances", key);
1495        Ok(())
1496    }
1497
1498    /// Invalidate all keys matching a pattern
1499    ///
1500    /// This scans L2 cache for keys matching the pattern, removes them from all tiers,
1501    /// and broadcasts the invalidation. L1 caches will be cleared via broadcast.
1502    ///
1503    /// Supports both legacy 2-tier mode and new multi-tier mode (v0.5.0+).
1504    ///
1505    /// **Note**: Pattern scanning requires a concrete `L2Cache` instance with `scan_keys()`.
1506    /// In multi-tier mode, this scans from L2 but removes from all tiers.
1507    ///
1508    /// # Arguments
1509    /// * `pattern` - Glob-style pattern (e.g., "user:*", "product:123:*")
1510    ///
1511    /// # Example
1512    /// ```rust,no_run
1513    /// # use multi_tier_cache::CacheManager;
1514    /// # async fn example(cache_manager: &CacheManager) -> anyhow::Result<()> {
1515    /// // Invalidate all user caches
1516    /// cache_manager.invalidate_pattern("user:*").await?;
1517    ///
1518    /// // Invalidate specific user's related caches
1519    /// cache_manager.invalidate_pattern("user:123:*").await?;
1520    /// # Ok(())
1521    /// # }
1522    /// ```
1523    /// # Errors
1524    ///
1525    /// Returns an error if invalidation fails.
1526    pub async fn invalidate_pattern(&self, pattern: &str) -> CacheResult<()> {
1527        debug!(pattern = %pattern, "Invalidating pattern across all tiers");
1528
1529        // 1. Invalidate in all configured tiers
1530        for tier in &self.tiers {
1531            debug!(tier = %tier.tier_level, "Invalidating pattern in tier");
1532            tier.backend.remove_pattern(pattern).await?;
1533        }
1534
1535        // 2. Broadcast invalidation if publisher is configured
1536        #[cfg(feature = "redis")]
1537        {
1538            if let Some(publisher) = &self.invalidation_publisher {
1539                let msg = InvalidationMessage::remove_pattern(pattern);
1540                publisher.publish_with_origin(&msg, Some(&self.instance_id)).await?;
1541                debug!(pattern = %pattern, "Broadcasted pattern invalidation");
1542            }
1543        }
1544
1545        Ok(())
1546    }
1547
1548    /// Set value with automatic broadcast to all instances
1549    ///
1550    /// This is a write-through operation that updates the cache and
1551    /// broadcasts the update to all other instances automatically.
1552    ///
1553    /// # Arguments
1554    /// * `key` - Cache key
1555    /// * `value` - Value to cache
1556    /// * `strategy` - Cache strategy (determines TTL)
1557    ///
1558    /// # Example
1559    /// ```rust,no_run
1560    /// # use multi_tier_cache::{CacheManager, CacheStrategy};
1561    /// # use bytes::Bytes;
1562    /// # async fn example(cache_manager: &CacheManager) -> anyhow::Result<()> {
1563    /// // Update and broadcast in one call
1564    /// let data = Bytes::from("active");
1565    /// cache_manager.set_with_broadcast("user:123", data, CacheStrategy::MediumTerm).await?;
1566    /// # Ok(())
1567    /// # }
1568    /// ```
1569    /// # Errors
1570    ///
1571    /// Returns an error if cache set or broadcast fails.
1572    pub async fn set_with_broadcast(
1573        &self,
1574        key: &str,
1575        value: Bytes,
1576        strategy: CacheStrategy,
1577    ) -> CacheResult<()> {
1578        #[cfg(feature = "redis")]
1579        let ttl = strategy.to_duration();
1580
1581        // Set in local caches
1582        self.set_with_strategy(key, value.clone(), strategy).await?;
1583
1584        // Broadcast update if invalidation is enabled
1585        #[cfg(feature = "redis")]
1586        if let Some(publisher) = &self.invalidation_publisher {
1587            let msg = InvalidationMessage::update(key, value, Some(ttl));
1588            publisher.publish_with_origin(&msg, Some(&self.instance_id)).await?;
1589            self.invalidation_stats
1590                .messages_sent
1591                .fetch_add(1, Ordering::Relaxed);
1592        }
1593
1594        Ok(())
1595    }
1596
1597    /// Get unique instance ID of this `CacheManager`
1598    #[must_use]
1599    pub fn instance_id(&self) -> &str {
1600        &self.instance_id
1601    }
1602
1603    /// Get invalidation statistics
1604    ///
1605    /// Returns statistics about invalidation operations if invalidation is enabled.
1606    #[cfg(feature = "redis")]
1607    pub fn invalidation_stats(&self) -> Option<crate::invalidation::InvalidationStats> {
1608        #[cfg(feature = "redis")]
1609        {
1610            Some(self.invalidation_stats.snapshot())
1611        }
1612        #[cfg(not(feature = "redis"))]
1613        {
1614            None
1615        }
1616    }
1617}
1618
1619/// Cache Manager statistics
1620#[allow(dead_code)]
1621#[derive(Debug, Clone)]
1622pub struct CacheManagerStats {
1623    pub total_requests: u64,
1624    pub l1_hits: u64,
1625    pub l2_hits: u64,
1626    pub total_hits: u64,
1627    pub misses: u64,
1628    pub hit_rate: f64,
1629    pub l1_hit_rate: f64,
1630    pub promotions: usize,
1631    pub in_flight_requests: usize,
1632}
1633
1634#[cfg(test)]
1635mod tests {
1636    use super::*;
1637    use std::time::Duration;
1638
1639    #[tokio::test]
1640    async fn test_in_flight_cancellation_cleanup() {
1641        let l1 = Arc::new(crate::backends::DashMapCache::new());
1642        let l2 = Arc::new(crate::backends::DashMapCache::new());
1643        let manager = CacheManager::new_with_backends(
1644            l1,
1645            Arc::new(ProxyL1ToL2(l2)) as Arc<dyn L2CacheBackend>,
1646            None,
1647        )
1648        .unwrap();
1649
1650        let key = "cancellation_test_key";
1651        let manager_clone = Arc::new(manager);
1652        let manager_clone2 = Arc::clone(&manager_clone);
1653
1654        let handle = tokio::spawn(async move {
1655            let _ = manager_clone2
1656                .get_or_compute_with(key, CacheStrategy::ShortTerm, || async {
1657                    tokio::time::sleep(Duration::from_secs(10)).await;
1658                    Ok(Bytes::from("result"))
1659                })
1660                .await;
1661        });
1662
1663        tokio::time::sleep(Duration::from_millis(50)).await;
1664
1665        assert!(manager_clone.in_flight_requests.contains_key(key));
1666
1667        handle.abort();
1668        let _ = handle.await;
1669
1670        assert!(
1671            !manager_clone.in_flight_requests.contains_key(key),
1672            "Key was not cleaned up after cancellation"
1673        );
1674    }
1675}