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