1use crate::error::CacheResult;
7use crate::traits::StreamingBackend;
8use bytes::Bytes;
9use futures_util::StreamExt;
10use redis::AsyncCommands;
11use serde::{Deserialize, Serialize};
12use std::time::{Duration, SystemTime, UNIX_EPOCH};
13use tokio::sync::broadcast;
14use tracing::{error, info, warn};
15use uuid::Uuid;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(tag = "type")]
20pub enum InvalidationMessage {
21 Remove { key: String },
23
24 Update {
27 key: String,
28 #[serde(with = "serde_bytes_wrapper")]
29 value: Bytes,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 ttl_secs: Option<u64>,
32 },
33
34 RemovePattern { pattern: String },
37
38 RemoveBulk { keys: Vec<String> },
40}
41
42impl InvalidationMessage {
43 pub fn remove(key: impl Into<String>) -> Self {
45 Self::Remove { key: key.into() }
46 }
47
48 pub fn update(key: impl Into<String>, value: Bytes, ttl: Option<Duration>) -> Self {
50 Self::Update {
51 key: key.into(),
52 value,
53 ttl_secs: ttl.map(|d| d.as_secs()),
54 }
55 }
56
57 pub fn remove_pattern(pattern: impl Into<String>) -> Self {
59 Self::RemovePattern {
60 pattern: pattern.into(),
61 }
62 }
63
64 #[must_use]
66 pub fn remove_bulk(keys: Vec<String>) -> Self {
67 Self::RemoveBulk { keys }
68 }
69
70 pub fn to_json(&self) -> CacheResult<String> {
76 serde_json::to_string(self).map_err(|e| {
77 crate::error::CacheError::SerializationError(format!(
78 "Failed to serialize invalidation message: {e}"
79 ))
80 })
81 }
82
83 pub fn from_json(json: &str) -> CacheResult<Self> {
89 serde_json::from_str(json).map_err(|e| {
90 crate::error::CacheError::SerializationError(format!(
91 "Failed to deserialize invalidation message: {e}"
92 ))
93 })
94 }
95
96 pub fn ttl(&self) -> Option<Duration> {
98 match self {
99 Self::Update { ttl_secs, .. } => ttl_secs.map(Duration::from_secs),
100 _ => None,
101 }
102 }
103}
104
105mod serde_bytes_wrapper {
107 use bytes::Bytes;
108 use serde::{Deserialize, Deserializer, Serializer};
109
110 fn parse_hex_digit(b: u8) -> Option<u8> {
111 match b {
112 b'0'..=b'9' => Some(b - b'0'),
113 b'a'..=b'f' => Some(b - b'a' + 10),
114 b'A'..=b'F' => Some(b - b'A' + 10),
115 _ => None,
116 }
117 }
118
119 pub fn serialize<S>(bytes: &Bytes, serializer: S) -> Result<S::Ok, S::Error>
120 where
121 S: Serializer,
122 {
123 let mut hex_str = String::with_capacity(bytes.len() * 2);
124 for &b in bytes.as_ref() {
125 let high = match b >> 4 {
126 val @ 0..=9 => (b'0' + val) as char,
127 val @ 10..=15 => (b'a' + val - 10) as char,
128 _ => '0',
129 };
130 let low = match b & 0xf {
131 val @ 0..=9 => (b'0' + val) as char,
132 val @ 10..=15 => (b'a' + val - 10) as char,
133 _ => '0',
134 };
135 hex_str.push(high);
136 hex_str.push(low);
137 }
138 serializer.serialize_str(&hex_str)
139 }
140
141 pub fn deserialize<'de, D>(deserializer: D) -> Result<Bytes, D::Error>
142 where
143 D: Deserializer<'de>,
144 {
145 let hex_str = String::deserialize(deserializer)?;
146 let bytes_str = hex_str.as_bytes();
147 if bytes_str.len() % 2 != 0 {
148 return Err(serde::de::Error::custom("Odd-length hex string"));
149 }
150 let mut v = Vec::with_capacity(bytes_str.len() / 2);
151 for chunk in bytes_str.chunks_exact(2) {
152 if let [h, l] = chunk {
153 let high = parse_hex_digit(*h)
154 .ok_or_else(|| serde::de::Error::custom("Invalid hex character"))?;
155 let low = parse_hex_digit(*l)
156 .ok_or_else(|| serde::de::Error::custom("Invalid hex character"))?;
157 v.push((high << 4) | low);
158 }
159 }
160 Ok(Bytes::from(v))
161 }
162}
163
164#[derive(Debug, Clone)]
166pub struct InvalidationConfig {
167 pub channel: String,
169
170 pub auto_broadcast_on_write: bool,
172
173 pub enable_audit_stream: bool,
175
176 pub audit_stream: String,
178
179 pub audit_stream_maxlen: Option<usize>,
181}
182
183impl Default for InvalidationConfig {
184 fn default() -> Self {
185 Self {
186 channel: "cache:invalidate".to_string(),
187 auto_broadcast_on_write: false, enable_audit_stream: false,
189 audit_stream: "cache:invalidations".to_string(),
190 audit_stream_maxlen: Some(10000),
191 }
192 }
193}
194
195pub struct InvalidationPublisher {
197 connection: redis::aio::ConnectionManager,
198 config: InvalidationConfig,
199}
200
201impl InvalidationPublisher {
202 #[must_use]
204 pub fn new(connection: redis::aio::ConnectionManager, config: InvalidationConfig) -> Self {
205 Self { connection, config }
206 }
207
208 pub async fn publish(&mut self, message: &InvalidationMessage) -> CacheResult<()> {
214 let json = message.to_json()?;
215
216 let _: () = self
218 .connection
219 .publish(&self.config.channel, &json)
220 .await
221 .map_err(|e| {
222 crate::error::CacheError::InvalidationError(format!(
223 "Failed to publish invalidation message: {e}"
224 ))
225 })?;
226
227 if self.config.enable_audit_stream
229 && let Err(e) = self.publish_to_audit_stream(message).await
230 {
231 warn!("Failed to publish to audit stream: {}", e);
233 }
234
235 Ok(())
236 }
237
238 async fn publish_to_audit_stream(&mut self, message: &InvalidationMessage) -> CacheResult<()> {
240 let timestamp = SystemTime::now()
241 .duration_since(UNIX_EPOCH)
242 .unwrap_or(Duration::ZERO)
243 .as_secs()
244 .to_string();
245
246 let (type_str, key_str): (&str, &str);
248 let extra_str: String;
249
250 match message {
251 InvalidationMessage::Remove { key } => {
252 type_str = "remove";
253 key_str = key.as_str();
254 extra_str = String::new();
255 }
256 InvalidationMessage::Update { key, .. } => {
257 type_str = "update";
258 key_str = key.as_str();
259 extra_str = String::new();
260 }
261 InvalidationMessage::RemovePattern { pattern } => {
262 type_str = "remove_pattern";
263 key_str = pattern.as_str();
264 extra_str = String::new();
265 }
266 InvalidationMessage::RemoveBulk { keys } => {
267 type_str = "remove_bulk";
268 key_str = "";
269 extra_str = keys.len().to_string();
270 }
271 }
272
273 let mut fields = vec![("type", type_str), ("timestamp", timestamp.as_str())];
274
275 if !key_str.is_empty() {
276 fields.push(("key", key_str));
277 }
278 if !extra_str.is_empty() {
279 fields.push(("count", extra_str.as_str()));
280 }
281
282 let mut cmd = redis::cmd("XADD");
283 cmd.arg(&self.config.audit_stream);
284
285 if let Some(maxlen) = self.config.audit_stream_maxlen {
286 cmd.arg("MAXLEN").arg("~").arg(maxlen);
287 }
288
289 cmd.arg("*"); for (key, value) in fields {
292 cmd.arg(key).arg(value);
293 }
294
295 let _: String = cmd.query_async(&mut self.connection).await.map_err(|e| {
296 crate::error::CacheError::BackendError(format!("Failed to add to audit stream: {e}"))
297 })?;
298
299 Ok(())
300 }
301}
302
303#[derive(Debug, Default, Clone)]
305pub struct InvalidationStats {
306 pub messages_sent: u64,
308
309 pub messages_received: u64,
311
312 pub removes_received: u64,
314
315 pub updates_received: u64,
317
318 pub patterns_received: u64,
320
321 pub bulk_removes_received: u64,
323
324 pub processing_errors: u64,
326}
327
328use std::sync::atomic::{AtomicU64, Ordering};
329
330#[derive(Debug, Default)]
332pub struct AtomicInvalidationStats {
333 pub messages_sent: AtomicU64,
334 pub messages_received: AtomicU64,
335 pub removes_received: AtomicU64,
336 pub updates_received: AtomicU64,
337 pub patterns_received: AtomicU64,
338 pub bulk_removes_received: AtomicU64,
339 pub processing_errors: AtomicU64,
340}
341
342impl AtomicInvalidationStats {
343 pub fn snapshot(&self) -> InvalidationStats {
344 InvalidationStats {
345 messages_sent: self.messages_sent.load(Ordering::Relaxed),
346 messages_received: self.messages_received.load(Ordering::Relaxed),
347 removes_received: self.removes_received.load(Ordering::Relaxed),
348 updates_received: self.updates_received.load(Ordering::Relaxed),
349 patterns_received: self.patterns_received.load(Ordering::Relaxed),
350 bulk_removes_received: self.bulk_removes_received.load(Ordering::Relaxed),
351 processing_errors: self.processing_errors.load(Ordering::Relaxed),
352 }
353 }
354}
355
356use std::sync::Arc;
357
358pub struct InvalidationSubscriber {
363 client: redis::Client,
365 config: InvalidationConfig,
367 stats: Arc<AtomicInvalidationStats>,
369 shutdown_tx: broadcast::Sender<()>,
371}
372
373impl InvalidationSubscriber {
374 pub fn new(redis_url: &str, config: InvalidationConfig) -> CacheResult<Self> {
383 let client = redis::Client::open(redis_url).map_err(|e| {
384 crate::error::CacheError::ConfigError(format!(
385 "Failed to create Redis client for subscriber: {e}"
386 ))
387 })?;
388
389 let (shutdown_tx, _) = broadcast::channel(1);
390
391 Ok(Self {
392 client,
393 config,
394 stats: Arc::new(AtomicInvalidationStats::default()),
395 shutdown_tx,
396 })
397 }
398
399 #[must_use]
401 pub fn stats(&self) -> InvalidationStats {
402 self.stats.snapshot()
403 }
404
405 pub fn start<F, Fut>(&self, handler: F) -> tokio::task::JoinHandle<()>
413 where
414 F: Fn(InvalidationMessage) -> Fut + Send + Sync + 'static,
415 Fut: std::future::Future<Output = CacheResult<()>> + Send + 'static,
416 {
417 let client = self.client.clone();
418 let channel = self.config.channel.clone();
419 let stats = Arc::clone(&self.stats);
420 let mut shutdown_rx = self.shutdown_tx.subscribe();
421
422 tokio::spawn(async move {
423 let handler = Arc::new(handler);
424
425 loop {
426 if shutdown_rx.try_recv().is_ok() {
428 info!("Invalidation subscriber shutting down...");
429 break;
430 }
431
432 match Self::run_subscriber_loop(
434 &client,
435 &channel,
436 Arc::clone(&handler),
437 Arc::clone(&stats),
438 &mut shutdown_rx,
439 )
440 .await
441 {
442 Ok(()) => {
443 info!("Invalidation subscriber loop completed normally");
444 break;
445 }
446 Err(e) => {
447 error!(
448 "Invalidation subscriber error: {}. Reconnecting in 5s...",
449 e
450 );
451 stats.processing_errors.fetch_add(1, Ordering::Relaxed);
452
453 tokio::select! {
455 () = tokio::time::sleep(Duration::from_secs(5)) => {},
456 _ = shutdown_rx.recv() => {
457 info!("Invalidation subscriber shutting down...");
458 break;
459 }
460 }
461 }
462 }
463 }
464 })
465 }
466
467 async fn run_subscriber_loop<F, Fut>(
469 client: &redis::Client,
470 channel: &str,
471 handler: Arc<F>,
472 stats: Arc<AtomicInvalidationStats>,
473 shutdown_rx: &mut broadcast::Receiver<()>,
474 ) -> CacheResult<()>
475 where
476 F: Fn(InvalidationMessage) -> Fut + Send + Sync + 'static,
477 Fut: std::future::Future<Output = CacheResult<()>> + Send + 'static,
478 {
479 let mut pubsub = client.get_async_pubsub().await.map_err(|e| {
480 crate::error::CacheError::BackendError(format!("Failed to get pubsub connection: {e}"))
481 })?;
482
483 pubsub.subscribe(channel).await.map_err(|e| {
485 crate::error::CacheError::InvalidationError(format!(
486 "Failed to subscribe to channel: {e}"
487 ))
488 })?;
489
490 info!("Subscribed to invalidation channel: {}", channel);
491
492 let mut stream = pubsub.on_message();
494
495 loop {
496 tokio::select! {
498 msg_result = stream.next() => {
499 match msg_result {
500 Some(msg) => {
501 let payload: String = match msg.get_payload() {
503 Ok(p) => p,
504 Err(e) => {
505 warn!("Failed to get message payload: {}", e);
506 stats.processing_errors.fetch_add(1, Ordering::Relaxed);
507 continue;
508 }
509 };
510
511 let invalidation_msg = match InvalidationMessage::from_json(&payload) {
513 Ok(m) => m,
514 Err(e) => {
515 warn!("Failed to deserialize invalidation message: {}", e);
516 stats.processing_errors.fetch_add(1, Ordering::Relaxed);
517 continue;
518 }
519 };
520
521 stats.messages_received.fetch_add(1, Ordering::Relaxed);
523 match &invalidation_msg {
524 InvalidationMessage::Remove { .. } => {
525 stats.removes_received.fetch_add(1, Ordering::Relaxed);
526 }
527 InvalidationMessage::Update { .. } => {
528 stats.updates_received.fetch_add(1, Ordering::Relaxed);
529 }
530 InvalidationMessage::RemovePattern { .. } => {
531 stats.patterns_received.fetch_add(1, Ordering::Relaxed);
532 }
533 InvalidationMessage::RemoveBulk { .. } => {
534 stats.bulk_removes_received.fetch_add(1, Ordering::Relaxed);
535 }
536 }
537
538 if let Err(e) = handler(invalidation_msg).await {
540 error!("Invalidation handler error: {}", e);
541 stats.processing_errors.fetch_add(1, Ordering::Relaxed);
542 }
543 }
544 None => {
545 return Err(crate::error::CacheError::InvalidationError("Pub/Sub message stream ended".to_string()));
547 }
548 }
549 }
550 _ = shutdown_rx.recv() => {
551 return Ok(());
552 }
553 }
554 }
555 }
556
557 pub fn shutdown(&self) {
559 let _ = self.shutdown_tx.send(());
560 }
561}
562
563pub struct ReliableStreamSubscriber {
565 redis_url: String,
566 config: InvalidationConfig,
567 stats: Arc<AtomicInvalidationStats>,
568 shutdown_tx: broadcast::Sender<()>,
569 group_name: String,
570 consumer_name: String,
571}
572
573impl ReliableStreamSubscriber {
574 pub fn new(redis_url: &str, config: InvalidationConfig, group_name: &str) -> CacheResult<Self> {
580 let _client = redis::Client::open(redis_url).map_err(|e| {
581 crate::error::CacheError::ConfigError(format!(
582 "Failed to create Redis client for reliable subscriber: {e}"
583 ))
584 })?;
585
586 let (shutdown_tx, _) = broadcast::channel(1);
587 let consumer_name = format!("consumer-{}", Uuid::new_v4());
588
589 Ok(Self {
590 redis_url: redis_url.to_string(),
591 config,
592 stats: Arc::new(AtomicInvalidationStats::default()),
593 shutdown_tx,
594 group_name: group_name.to_string(),
595 consumer_name,
596 })
597 }
598
599 pub fn start<F, Fut>(&self, handler: F) -> tokio::task::JoinHandle<()>
600 where
601 F: Fn(InvalidationMessage) -> Fut + Send + Sync + 'static,
602 Fut: std::future::Future<Output = CacheResult<()>> + Send + 'static,
603 {
604 let stream_key = self.config.channel.clone();
605 let group_name = self.group_name.clone();
606 let consumer_name = self.consumer_name.clone();
607 let handler = Arc::new(handler);
608 let stats = self.stats.clone();
609 let mut shutdown_rx = self.shutdown_tx.subscribe();
610 let redis_url = self.redis_url.clone();
611
612 tokio::spawn(async move {
613 info!(
614 stream = %stream_key,
615 group = %group_name,
616 consumer = %consumer_name,
617 "Starting reliable stream subscriber"
618 );
619
620 let redis_backend = crate::redis_streams::RedisStreams::new(&redis_url).await;
622 match redis_backend {
623 Ok(backend) => {
624 let _ = backend
625 .stream_create_group(&stream_key, &group_name, "0")
626 .await;
627
628 loop {
629 if shutdown_rx.try_recv().is_ok() {
631 break;
632 }
633
634 if let Err(e) = Self::run_reliable_loop(
635 &backend,
636 &stream_key,
637 &group_name,
638 &consumer_name,
639 handler.clone(),
640 stats.clone(),
641 &mut shutdown_rx,
642 )
643 .await
644 {
645 error!("Reliable subscriber loop error: {}", e);
646
647 tokio::select! {
648 () = tokio::time::sleep(Duration::from_secs(5)) => {},
649 _ = shutdown_rx.recv() => break,
650 }
651 } else {
652 break; }
654 }
655 }
656 Err(e) => {
657 error!(
658 "Failed to initialize Redis Streams backend for reliable subscriber: {}",
659 e
660 );
661 }
662 }
663 })
664 }
665
666 async fn run_reliable_loop<F, Fut>(
667 backend: &dyn crate::traits::StreamingBackend,
668 stream_key: &str,
669 group_name: &str,
670 consumer_name: &str,
671 handler: Arc<F>,
672 stats: Arc<AtomicInvalidationStats>,
673 shutdown_rx: &mut broadcast::Receiver<()>,
674 ) -> CacheResult<()>
675 where
676 F: Fn(InvalidationMessage) -> Fut + Send + Sync + 'static,
677 Fut: std::future::Future<Output = CacheResult<()>> + Send + 'static,
678 {
679 loop {
680 tokio::select! {
681 entries_result = backend.stream_read_group(stream_key, group_name, consumer_name, 10, Some(5000)) => {
682 let entries = entries_result?;
683 if entries.is_empty() { continue; }
684
685 let mut processed_ids = Vec::new();
686 for (id, fields) in entries {
687 let payload = fields.iter().find(|(k, _)| k == "payload")
689 .map(|(_, v)| v.as_str())
690 .or_else(|| fields.first().map(|(_, v)| v.as_str()));
691
692 if let Some(msg) = payload.and_then(|json| InvalidationMessage::from_json(json).ok()) {
693 stats.messages_received.fetch_add(1, Ordering::Relaxed);
694 if let Err(e) = handler(msg).await {
695 error!("Reliable handler error: {}", e);
696 stats.processing_errors.fetch_add(1, Ordering::Relaxed);
697 } else {
698 processed_ids.push(id);
699 }
700 }
701 }
702
703 if !processed_ids.is_empty() {
704 backend.stream_ack(stream_key, group_name, &processed_ids).await?;
705 }
706 }
707 _ = shutdown_rx.recv() => return Ok(()),
708 }
709 }
710 }
711
712 pub fn shutdown(&self) {
714 let _ = self.shutdown_tx.send(()).unwrap_or(0);
715 }
716}
717
718#[cfg(test)]
719mod tests {
720 use super::*;
721
722 #[test]
723 fn test_invalidation_message_serialization() -> CacheResult<()> {
724 let msg = InvalidationMessage::remove("test_key");
726 let json = msg.to_json()?;
727 let parsed = InvalidationMessage::from_json(&json)?;
728 match parsed {
729 InvalidationMessage::Remove { key } => assert_eq!(key, "test_key"),
730 _ => panic!("Wrong message type"),
731 }
732
733 let msg = InvalidationMessage::update(
735 "test_key",
736 Bytes::from("{\"value\": 123}"),
737 Some(Duration::from_secs(3600)),
738 );
739
740 if let InvalidationMessage::Update {
741 key,
742 value,
743 ttl_secs,
744 } = msg
745 {
746 assert_eq!(key, "test_key");
747 assert_eq!(value, Bytes::from("{\"value\": 123}"));
748 assert_eq!(ttl_secs, Some(3600));
749 } else {
750 panic!("Expected Update message");
751 }
752
753 let msg = InvalidationMessage::remove_pattern("user:*");
755 let json = msg.to_json()?;
756 let parsed = InvalidationMessage::from_json(&json)?;
757 match parsed {
758 InvalidationMessage::RemovePattern { pattern } => assert_eq!(pattern, "user:*"),
759 _ => panic!("Wrong message type"),
760 }
761
762 let msg = InvalidationMessage::remove_bulk(vec!["key1".to_string(), "key2".to_string()]);
764 let json = msg.to_json()?;
765 let parsed = InvalidationMessage::from_json(&json)?;
766 match parsed {
767 InvalidationMessage::RemoveBulk { keys } => assert_eq!(keys, vec!["key1", "key2"]),
768 _ => panic!("Wrong message type"),
769 }
770 Ok(())
771 }
772
773 #[test]
774 fn test_invalidation_config_default() {
775 let config = InvalidationConfig::default();
776 assert_eq!(config.channel, "cache:invalidate");
777 assert!(!config.auto_broadcast_on_write);
778 assert!(!config.enable_audit_stream);
779 }
780}