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}