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