1use futures::StreamExt;
13use redis::{
14 aio::{ConnectionLike, MultiplexedConnection, PubSub},
15 cluster::ClusterClient,
16 cluster_async::ClusterConnection,
17 streams::StreamReadReply,
18 AsyncCommands, Cmd, FromRedisValue, Pipeline, RedisFuture, Value,
19};
20use serde::{de::DeserializeOwned, Serialize};
21use std::{
22 collections::{HashMap, HashSet},
23 fmt,
24 future::Future,
25 marker::PhantomData,
26 sync::atomic::{AtomicU64, Ordering},
27 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
28};
29use tokio::sync::{mpsc, oneshot, OnceCell};
30
31use crate::{
32 cache::jittered_ttl, CacheStats, CounterVec, HistogramOptions, HistogramVec, Metrics,
33 MetricsError, SingleFlight, SingleFlightError, VectorOptions,
34};
35
36#[cfg(feature = "telemetry")]
37use crate::{TelemetrySpan, TelemetrySpanKind};
38
39const RELEASE_LOCK: &str =
40 "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
41const EXTEND_LOCK: &str =
42 "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end";
43
44static TOKEN_COUNTER: AtomicU64 = AtomicU64::new(0);
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct RedisStoreConfig {
48 pub urls: Vec<String>,
49 pub cluster: bool,
50 pub key_prefix: String,
51 pub operation_timeout: Duration,
52}
53
54impl RedisStoreConfig {
55 pub fn new(url: impl Into<String>) -> Self {
56 Self {
57 urls: vec![url.into()],
58 cluster: false,
59 key_prefix: String::new(),
60 operation_timeout: Duration::from_secs(3),
61 }
62 }
63
64 pub fn cluster<I, S>(nodes: I) -> Self
65 where
66 I: IntoIterator<Item = S>,
67 S: Into<String>,
68 {
69 Self {
70 urls: nodes.into_iter().map(Into::into).collect(),
71 cluster: true,
72 key_prefix: String::new(),
73 operation_timeout: Duration::from_secs(3),
74 }
75 }
76
77 pub fn with_key_prefix(mut self, prefix: impl Into<String>) -> Self {
78 self.key_prefix = prefix.into();
79 self
80 }
81
82 pub fn with_operation_timeout(mut self, timeout: Duration) -> Self {
83 assert!(
84 !timeout.is_zero(),
85 "Redis operation timeout must be positive"
86 );
87 self.operation_timeout = timeout;
88 self
89 }
90}
91
92#[derive(Clone)]
93pub struct RedisStore {
94 client: RedisClient,
95 connection: std::sync::Arc<OnceCell<RedisConnection>>,
96 config: RedisStoreConfig,
97 metrics: Option<RedisStoreMetrics>,
98}
99
100#[derive(Clone)]
102pub struct RedisStoreMetrics {
103 operations: CounterVec,
104 duration: HistogramVec,
105}
106
107impl RedisStoreMetrics {
108 pub fn register(metrics: &Metrics) -> Result<Self, MetricsError> {
109 let labels = ["operation", "outcome"];
110 Ok(Self {
111 operations: metrics.counter_vec(
112 VectorOptions::new("operations_total", "Completed Redis store operations")
113 .with_namespace("rust_zero")
114 .with_subsystem("redis")
115 .with_labels(labels),
116 )?,
117 duration: metrics.histogram_vec(
118 HistogramOptions::new(
119 "operation_duration_seconds",
120 "Redis store operation latency",
121 )
122 .with_vector_options(
123 VectorOptions::new(
124 "operation_duration_seconds",
125 "Redis store operation latency",
126 )
127 .with_namespace("rust_zero")
128 .with_subsystem("redis")
129 .with_labels(labels),
130 ),
131 )?,
132 })
133 }
134
135 fn observe(&self, operation: &str, outcome: &str, elapsed: Duration) {
136 let labels = [operation, outcome];
137 let _ = self.operations.inc(&labels);
138 let _ = self.duration.observe(elapsed.as_secs_f64(), &labels);
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub struct RedisSubscriptionConfig {
145 pub capacity: usize,
147 pub reconnect_delay: Duration,
149 pub max_reconnect_delay: Duration,
151}
152
153impl Default for RedisSubscriptionConfig {
154 fn default() -> Self {
155 Self {
156 capacity: 256,
157 reconnect_delay: Duration::from_millis(100),
158 max_reconnect_delay: Duration::from_secs(5),
159 }
160 }
161}
162
163impl RedisSubscriptionConfig {
164 pub fn with_capacity(mut self, capacity: usize) -> Self {
165 self.capacity = capacity;
166 self
167 }
168
169 pub fn with_reconnect_delay(mut self, delay: Duration) -> Self {
170 self.reconnect_delay = delay;
171 self
172 }
173
174 pub fn with_max_reconnect_delay(mut self, delay: Duration) -> Self {
175 self.max_reconnect_delay = delay;
176 self
177 }
178
179 fn validate(self) -> Result<Self, RedisStoreError> {
180 if self.capacity < 2 {
181 return Err(RedisStoreError::InvalidArgument(
182 "Redis subscription capacity must be at least two",
183 ));
184 }
185 if self.reconnect_delay.is_zero() || self.max_reconnect_delay < self.reconnect_delay {
186 return Err(RedisStoreError::InvalidArgument(
187 "Redis subscription reconnect delays must be positive and ordered",
188 ));
189 }
190 Ok(self)
191 }
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct RedisSubscriptionMessage {
197 pub channel: String,
198 pub pattern: Option<String>,
199 pub payload: Vec<u8>,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
204pub enum RedisSubscriptionEvent {
205 Message(RedisSubscriptionMessage),
206 Lagged {
208 dropped: u64,
209 },
210 Disconnected {
212 error: String,
213 retry_in: Duration,
214 },
215 Reconnected,
217 Closed,
219}
220
221pub struct RedisSubscription {
223 receiver: mpsc::Receiver<RedisSubscriptionEvent>,
224 shutdown: Option<oneshot::Sender<()>>,
225}
226
227impl RedisSubscription {
228 pub async fn recv(&mut self) -> Option<RedisSubscriptionEvent> {
229 self.receiver.recv().await
230 }
231
232 pub fn try_recv(&mut self) -> Result<RedisSubscriptionEvent, mpsc::error::TryRecvError> {
233 self.receiver.try_recv()
234 }
235
236 pub fn shutdown(&mut self) {
238 if let Some(shutdown) = self.shutdown.take() {
239 let _ = shutdown.send(());
240 }
241 }
242
243 pub async fn close(mut self) {
245 self.shutdown();
246 while let Some(event) = self.recv().await {
247 if event == RedisSubscriptionEvent::Closed {
248 break;
249 }
250 }
251 }
252}
253
254impl Drop for RedisSubscription {
255 fn drop(&mut self) {
256 self.shutdown();
257 }
258}
259
260#[derive(Debug, Clone, Copy)]
261enum RedisSubscriptionMode {
262 Channels,
263 Patterns,
264}
265
266impl RedisStore {
267 pub fn new(config: RedisStoreConfig) -> Result<Self, RedisStoreError> {
268 let client = if config.cluster {
269 RedisClient::Cluster(ClusterClient::new(config.urls.clone())?)
270 } else {
271 let url = config
272 .urls
273 .first()
274 .ok_or(RedisStoreError::MissingEndpoint)?;
275 RedisClient::Standalone(redis::Client::open(url.as_str())?)
276 };
277 Ok(Self {
278 client,
279 connection: std::sync::Arc::new(OnceCell::new()),
280 config,
281 metrics: None,
282 })
283 }
284
285 pub fn with_metrics(mut self, metrics: RedisStoreMetrics) -> Self {
287 self.metrics = Some(metrics);
288 self
289 }
290
291 pub async fn ping(&self) -> Result<(), RedisStoreError> {
292 let mut connection = self.connection().await?;
293 self.run(
294 "ping",
295 redis::cmd("PING").query_async::<String>(&mut connection),
296 )
297 .await?;
298 Ok(())
299 }
300
301 pub async fn do_command<T: FromRedisValue>(
307 &self,
308 mut command: Cmd,
309 ) -> Result<T, RedisStoreError> {
310 self.query(&mut command).await
311 }
312
313 pub fn prefixed_key(&self, key: &str) -> String {
315 self.key(key)
316 }
317
318 pub async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, RedisStoreError> {
319 let mut connection = self.connection().await?;
320 let key = self.key(key);
321 self.run("get", connection.get(key)).await
322 }
323
324 pub async fn get_json<T: DeserializeOwned>(
325 &self,
326 key: &str,
327 ) -> Result<Option<T>, RedisStoreError> {
328 self.get(key)
329 .await?
330 .map(|value| serde_json::from_slice(&value).map_err(RedisStoreError::Json))
331 .transpose()
332 }
333
334 pub async fn get_string(&self, key: &str) -> Result<Option<String>, RedisStoreError> {
335 let mut command = redis::cmd("GET");
336 command.arg(self.key(key));
337 self.query(&mut command).await
338 }
339
340 pub async fn set(
341 &self,
342 key: &str,
343 value: impl AsRef<[u8]>,
344 ttl: Option<Duration>,
345 ) -> Result<(), RedisStoreError> {
346 let mut connection = self.connection().await?;
347 let key = self.key(key);
348 match ttl {
349 Some(ttl) if ttl.is_zero() => Err(RedisStoreError::InvalidTtl),
350 Some(ttl) => {
351 self.run(
352 "set",
353 connection.pset_ex(key, value.as_ref(), duration_millis(ttl)?),
354 )
355 .await
356 }
357 None => self.run("set", connection.set(key, value.as_ref())).await,
358 }
359 }
360
361 pub async fn set_json<T: Serialize>(
362 &self,
363 key: &str,
364 value: &T,
365 ttl: Option<Duration>,
366 ) -> Result<(), RedisStoreError> {
367 self.set(key, serde_json::to_vec(value)?, ttl).await
368 }
369
370 pub async fn set_if_absent(
371 &self,
372 key: &str,
373 value: impl AsRef<[u8]>,
374 ttl: Option<Duration>,
375 ) -> Result<bool, RedisStoreError> {
376 if ttl.is_some_and(|ttl| ttl.is_zero()) {
377 return Err(RedisStoreError::InvalidTtl);
378 }
379 let mut command = redis::cmd("SET");
380 command.arg(self.key(key)).arg(value.as_ref()).arg("NX");
381 if let Some(ttl) = ttl {
382 command.arg("PX").arg(duration_millis(ttl)?);
383 }
384 Ok(self.query::<Option<String>>(&mut command).await?.is_some())
385 }
386
387 pub async fn get_many(&self, keys: &[&str]) -> Result<Vec<Option<Vec<u8>>>, RedisStoreError> {
388 if keys.is_empty() {
389 return Ok(Vec::new());
390 }
391 let mut command = redis::cmd("MGET");
392 command.arg(keys.iter().map(|key| self.key(key)).collect::<Vec<_>>());
393 self.query(&mut command).await
394 }
395
396 pub async fn set_many<V: AsRef<[u8]>>(
397 &self,
398 entries: &[(&str, V)],
399 ) -> Result<(), RedisStoreError> {
400 if entries.is_empty() {
401 return Ok(());
402 }
403 let mut command = redis::cmd("MSET");
404 for (key, value) in entries {
405 command.arg(self.key(key)).arg(value.as_ref());
406 }
407 self.query(&mut command).await
408 }
409
410 pub async fn delete(&self, keys: &[&str]) -> Result<u64, RedisStoreError> {
411 if keys.is_empty() {
412 return Ok(0);
413 }
414 let mut connection = self.connection().await?;
415 let keys: Vec<_> = keys.iter().map(|key| self.key(key)).collect();
416 self.run("delete", connection.del(keys)).await
417 }
418
419 pub async fn exists(&self, key: &str) -> Result<bool, RedisStoreError> {
420 let mut connection = self.connection().await?;
421 let key = self.key(key);
422 self.run("exists", connection.exists(key)).await
423 }
424
425 pub async fn increment(&self, key: &str, amount: i64) -> Result<i64, RedisStoreError> {
426 let mut connection = self.connection().await?;
427 let key = self.key(key);
428 self.run("increment", connection.incr(key, amount)).await
429 }
430
431 pub async fn decrement(&self, key: &str, amount: i64) -> Result<i64, RedisStoreError> {
432 let mut command = redis::cmd("DECRBY");
433 command.arg(self.key(key)).arg(amount);
434 self.query(&mut command).await
435 }
436
437 pub async fn expire(&self, key: &str, ttl: Duration) -> Result<bool, RedisStoreError> {
438 if ttl.is_zero() {
439 return Err(RedisStoreError::InvalidTtl);
440 }
441 let mut command = redis::cmd("PEXPIRE");
442 command.arg(self.key(key)).arg(duration_millis(ttl)?);
443 self.query(&mut command).await
444 }
445
446 pub async fn persist(&self, key: &str) -> Result<bool, RedisStoreError> {
447 let mut command = redis::cmd("PERSIST");
448 command.arg(self.key(key));
449 self.query(&mut command).await
450 }
451
452 pub async fn ttl(&self, key: &str) -> Result<RedisTtl, RedisStoreError> {
453 let mut command = redis::cmd("PTTL");
454 command.arg(self.key(key));
455 match self.query::<i64>(&mut command).await? {
456 -2 => Ok(RedisTtl::Missing),
457 -1 => Ok(RedisTtl::Persistent),
458 millis if millis >= 0 => Ok(RedisTtl::ExpiresIn(Duration::from_millis(millis as u64))),
459 value => Err(RedisStoreError::UnexpectedResponse(format!(
460 "PTTL returned {value}"
461 ))),
462 }
463 }
464
465 pub async fn hash_get(
466 &self,
467 key: &str,
468 field: &str,
469 ) -> Result<Option<Vec<u8>>, RedisStoreError> {
470 let mut command = redis::cmd("HGET");
471 command.arg(self.key(key)).arg(field);
472 self.query(&mut command).await
473 }
474
475 pub async fn hash_set(
476 &self,
477 key: &str,
478 field: &str,
479 value: impl AsRef<[u8]>,
480 ) -> Result<bool, RedisStoreError> {
481 let mut command = redis::cmd("HSET");
482 command.arg(self.key(key)).arg(field).arg(value.as_ref());
483 self.query(&mut command).await
484 }
485
486 pub async fn hash_get_all(
487 &self,
488 key: &str,
489 ) -> Result<HashMap<Vec<u8>, Vec<u8>>, RedisStoreError> {
490 let mut command = redis::cmd("HGETALL");
491 command.arg(self.key(key));
492 self.query(&mut command).await
493 }
494
495 pub async fn hash_delete(&self, key: &str, fields: &[&str]) -> Result<u64, RedisStoreError> {
496 if fields.is_empty() {
497 return Ok(0);
498 }
499 let mut command = redis::cmd("HDEL");
500 command.arg(self.key(key)).arg(fields);
501 self.query(&mut command).await
502 }
503
504 pub async fn hash_increment(
505 &self,
506 key: &str,
507 field: &str,
508 amount: i64,
509 ) -> Result<i64, RedisStoreError> {
510 let mut command = redis::cmd("HINCRBY");
511 command.arg(self.key(key)).arg(field).arg(amount);
512 self.query(&mut command).await
513 }
514
515 pub async fn list_push_front<V: AsRef<[u8]>>(
516 &self,
517 key: &str,
518 values: &[V],
519 ) -> Result<u64, RedisStoreError> {
520 self.list_push("LPUSH", key, values).await
521 }
522
523 pub async fn list_push_back<V: AsRef<[u8]>>(
524 &self,
525 key: &str,
526 values: &[V],
527 ) -> Result<u64, RedisStoreError> {
528 self.list_push("RPUSH", key, values).await
529 }
530
531 pub async fn list_pop_front(&self, key: &str) -> Result<Option<Vec<u8>>, RedisStoreError> {
532 let mut command = redis::cmd("LPOP");
533 command.arg(self.key(key));
534 self.query(&mut command).await
535 }
536
537 pub async fn list_pop_back(&self, key: &str) -> Result<Option<Vec<u8>>, RedisStoreError> {
538 let mut command = redis::cmd("RPOP");
539 command.arg(self.key(key));
540 self.query(&mut command).await
541 }
542
543 pub async fn list_range(
544 &self,
545 key: &str,
546 start: isize,
547 stop: isize,
548 ) -> Result<Vec<Vec<u8>>, RedisStoreError> {
549 let mut command = redis::cmd("LRANGE");
550 command.arg(self.key(key)).arg(start).arg(stop);
551 self.query(&mut command).await
552 }
553
554 pub async fn list_len(&self, key: &str) -> Result<u64, RedisStoreError> {
555 let mut command = redis::cmd("LLEN");
556 command.arg(self.key(key));
557 self.query(&mut command).await
558 }
559
560 pub async fn set_add<V: AsRef<[u8]>>(
561 &self,
562 key: &str,
563 members: &[V],
564 ) -> Result<u64, RedisStoreError> {
565 self.set_members_command("SADD", key, members).await
566 }
567
568 pub async fn set_remove<V: AsRef<[u8]>>(
569 &self,
570 key: &str,
571 members: &[V],
572 ) -> Result<u64, RedisStoreError> {
573 self.set_members_command("SREM", key, members).await
574 }
575
576 pub async fn set_members(&self, key: &str) -> Result<HashSet<Vec<u8>>, RedisStoreError> {
577 let mut command = redis::cmd("SMEMBERS");
578 command.arg(self.key(key));
579 self.query(&mut command).await
580 }
581
582 pub async fn set_contains(
583 &self,
584 key: &str,
585 member: impl AsRef<[u8]>,
586 ) -> Result<bool, RedisStoreError> {
587 let mut command = redis::cmd("SISMEMBER");
588 command.arg(self.key(key)).arg(member.as_ref());
589 self.query(&mut command).await
590 }
591
592 pub async fn set_len(&self, key: &str) -> Result<u64, RedisStoreError> {
593 let mut command = redis::cmd("SCARD");
594 command.arg(self.key(key));
595 self.query(&mut command).await
596 }
597
598 pub async fn sorted_set_add(
599 &self,
600 key: &str,
601 score: f64,
602 member: impl AsRef<[u8]>,
603 ) -> Result<bool, RedisStoreError> {
604 let mut command = redis::cmd("ZADD");
605 command.arg(self.key(key)).arg(score).arg(member.as_ref());
606 self.query(&mut command).await
607 }
608
609 pub async fn sorted_set_remove<V: AsRef<[u8]>>(
610 &self,
611 key: &str,
612 members: &[V],
613 ) -> Result<u64, RedisStoreError> {
614 if members.is_empty() {
615 return Ok(0);
616 }
617 let mut command = redis::cmd("ZREM");
618 command.arg(self.key(key));
619 for member in members {
620 command.arg(member.as_ref());
621 }
622 self.query(&mut command).await
623 }
624
625 pub async fn sorted_set_range_with_scores(
626 &self,
627 key: &str,
628 start: isize,
629 stop: isize,
630 ) -> Result<Vec<(Vec<u8>, f64)>, RedisStoreError> {
631 let mut command = redis::cmd("ZRANGE");
632 command
633 .arg(self.key(key))
634 .arg(start)
635 .arg(stop)
636 .arg("WITHSCORES");
637 self.query(&mut command).await
638 }
639
640 pub async fn sorted_set_score(
641 &self,
642 key: &str,
643 member: impl AsRef<[u8]>,
644 ) -> Result<Option<f64>, RedisStoreError> {
645 let mut command = redis::cmd("ZSCORE");
646 command.arg(self.key(key)).arg(member.as_ref());
647 self.query(&mut command).await
648 }
649
650 pub async fn sorted_set_len(&self, key: &str) -> Result<u64, RedisStoreError> {
651 let mut command = redis::cmd("ZCARD");
652 command.arg(self.key(key));
653 self.query(&mut command).await
654 }
655
656 pub async fn publish(
657 &self,
658 channel: &str,
659 message: impl AsRef<[u8]>,
660 ) -> Result<u64, RedisStoreError> {
661 let mut command = redis::cmd("PUBLISH");
662 command.arg(self.key(channel)).arg(message.as_ref());
663 self.query(&mut command).await
664 }
665
666 pub async fn subscribe<I, S>(
668 &self,
669 channels: I,
670 config: RedisSubscriptionConfig,
671 ) -> Result<RedisSubscription, RedisStoreError>
672 where
673 I: IntoIterator<Item = S>,
674 S: AsRef<str>,
675 {
676 self.start_subscription(channels, config, RedisSubscriptionMode::Channels)
677 .await
678 }
679
680 pub async fn psubscribe<I, S>(
682 &self,
683 patterns: I,
684 config: RedisSubscriptionConfig,
685 ) -> Result<RedisSubscription, RedisStoreError>
686 where
687 I: IntoIterator<Item = S>,
688 S: AsRef<str>,
689 {
690 self.start_subscription(patterns, config, RedisSubscriptionMode::Patterns)
691 .await
692 }
693
694 pub async fn do_pipeline<T: FromRedisValue>(
699 &self,
700 pipeline: &Pipeline,
701 ) -> Result<T, RedisStoreError> {
702 let mut connection = self.connection().await?;
703 self.run("pipeline", pipeline.query_async(&mut connection))
704 .await
705 }
706
707 pub async fn eval<T: FromRedisValue, K: AsRef<str>, A: AsRef<[u8]>>(
709 &self,
710 script: &str,
711 keys: &[K],
712 arguments: &[A],
713 ) -> Result<T, RedisStoreError> {
714 let mut command = redis::cmd("EVAL");
715 command.arg(script).arg(keys.len());
716 for key in keys {
717 command.arg(self.key(key.as_ref()));
718 }
719 for argument in arguments {
720 command.arg(argument.as_ref());
721 }
722 self.query(&mut command).await
723 }
724
725 pub async fn stream_add<V: AsRef<[u8]>>(
727 &self,
728 key: &str,
729 id: Option<&str>,
730 fields: &[(&str, V)],
731 ) -> Result<String, RedisStoreError> {
732 if fields.is_empty() {
733 return Err(RedisStoreError::InvalidArgument(
734 "Redis stream entries require at least one field",
735 ));
736 }
737 let mut command = redis::cmd("XADD");
738 command.arg(self.key(key)).arg(id.unwrap_or("*"));
739 for (field, value) in fields {
740 command.arg(field).arg(value.as_ref());
741 }
742 self.query(&mut command).await
743 }
744
745 pub async fn stream_read(
747 &self,
748 streams: &[(&str, &str)],
749 count: Option<usize>,
750 block: Option<Duration>,
751 ) -> Result<StreamReadReply, RedisStoreError> {
752 let mut command = redis::cmd("XREAD");
753 append_stream_read_options(&mut command, count, block, false)?;
754 append_streams(&mut command, streams, |key| self.key(key))?;
755 self.query(&mut command).await
756 }
757
758 pub async fn stream_group_create(
760 &self,
761 key: &str,
762 group: &str,
763 id: &str,
764 create_stream: bool,
765 ) -> Result<(), RedisStoreError> {
766 let mut command = redis::cmd("XGROUP");
767 command.arg("CREATE").arg(self.key(key)).arg(group).arg(id);
768 if create_stream {
769 command.arg("MKSTREAM");
770 }
771 expect_ok(self.query(&mut command).await?, "XGROUP CREATE")
772 }
773
774 pub async fn stream_group_destroy(
776 &self,
777 key: &str,
778 group: &str,
779 ) -> Result<bool, RedisStoreError> {
780 let mut command = redis::cmd("XGROUP");
781 command.arg("DESTROY").arg(self.key(key)).arg(group);
782 self.query(&mut command).await
783 }
784
785 pub async fn stream_group_read(
787 &self,
788 group: &str,
789 consumer: &str,
790 streams: &[(&str, &str)],
791 count: Option<usize>,
792 block: Option<Duration>,
793 no_ack: bool,
794 ) -> Result<StreamReadReply, RedisStoreError> {
795 if group.is_empty() || consumer.is_empty() {
796 return Err(RedisStoreError::InvalidArgument(
797 "Redis stream group and consumer names must not be empty",
798 ));
799 }
800 let mut command = redis::cmd("XREADGROUP");
801 command.arg("GROUP").arg(group).arg(consumer);
802 append_stream_read_options(&mut command, count, block, no_ack)?;
803 append_streams(&mut command, streams, |key| self.key(key))?;
804 self.query(&mut command).await
805 }
806
807 pub async fn stream_ack(
809 &self,
810 key: &str,
811 group: &str,
812 ids: &[&str],
813 ) -> Result<u64, RedisStoreError> {
814 if ids.is_empty() {
815 return Ok(0);
816 }
817 let mut command = redis::cmd("XACK");
818 command.arg(self.key(key)).arg(group).arg(ids);
819 self.query(&mut command).await
820 }
821
822 pub async fn stream_pending(&self, key: &str, group: &str) -> Result<Value, RedisStoreError> {
824 let mut command = redis::cmd("XPENDING");
825 command.arg(self.key(key)).arg(group);
826 self.query(&mut command).await
827 }
828
829 pub async fn stream_claim(
834 &self,
835 key: &str,
836 group: &str,
837 consumer: &str,
838 min_idle: Duration,
839 ids: &[&str],
840 ) -> Result<Value, RedisStoreError> {
841 if ids.is_empty() {
842 return Err(RedisStoreError::InvalidArgument(
843 "Redis stream claim requires at least one entry ID",
844 ));
845 }
846 let mut command = redis::cmd("XCLAIM");
847 command
848 .arg(self.key(key))
849 .arg(group)
850 .arg(consumer)
851 .arg(duration_millis(min_idle)?)
852 .arg(ids);
853 self.query(&mut command).await
854 }
855
856 pub async fn stream_delete(&self, key: &str, ids: &[&str]) -> Result<u64, RedisStoreError> {
858 if ids.is_empty() {
859 return Ok(0);
860 }
861 let mut command = redis::cmd("XDEL");
862 command.arg(self.key(key)).arg(ids);
863 self.query(&mut command).await
864 }
865
866 pub async fn stream_group_set_id(
871 &self,
872 key: &str,
873 group: &str,
874 id: &str,
875 ) -> Result<(), RedisStoreError> {
876 let mut command = redis::cmd("XGROUP");
877 command.arg("SETID").arg(self.key(key)).arg(group).arg(id);
878 let response: String = self.query(&mut command).await?;
879 if response == "OK" {
880 Ok(())
881 } else {
882 Err(RedisStoreError::UnexpectedResponse(format!(
883 "XGROUP SETID returned {response}"
884 )))
885 }
886 }
887
888 pub fn lock(&self, key: impl Into<String>, ttl: Duration) -> RedisLock {
889 assert!(!ttl.is_zero(), "Redis lock TTL must be positive");
890 RedisLock {
891 store: self.clone(),
892 key: key.into(),
893 token: unique_token(),
894 ttl,
895 held: false,
896 }
897 }
898
899 async fn connection(&self) -> Result<RedisConnection, RedisStoreError> {
900 let connection = self.connection.get_or_try_init(|| async {
901 match &self.client {
902 RedisClient::Standalone(client) => client
903 .get_multiplexed_async_connection()
904 .await
905 .map(RedisConnection::Standalone),
906 RedisClient::Cluster(client) => client
907 .get_async_connection()
908 .await
909 .map(RedisConnection::Cluster),
910 }
911 });
912 tokio::time::timeout(self.config.operation_timeout, connection)
913 .await
914 .map_err(|_| RedisStoreError::Timeout)?
915 .cloned()
916 .map_err(RedisStoreError::Redis)
917 }
918
919 async fn start_subscription<I, S>(
920 &self,
921 topics: I,
922 subscription_config: RedisSubscriptionConfig,
923 mode: RedisSubscriptionMode,
924 ) -> Result<RedisSubscription, RedisStoreError>
925 where
926 I: IntoIterator<Item = S>,
927 S: AsRef<str>,
928 {
929 let subscription_config = subscription_config.validate()?;
930 let topics: Vec<String> = topics
931 .into_iter()
932 .map(|topic| topic.as_ref().to_owned())
933 .collect();
934 if topics.is_empty() || topics.iter().any(String::is_empty) {
935 return Err(RedisStoreError::InvalidArgument(
936 "Redis subscriptions require at least one non-empty channel or pattern",
937 ));
938 }
939 let topics: Vec<String> = topics.iter().map(|topic| self.key(topic)).collect();
940
941 let (pubsub, endpoint_index) = connect_subscription(
942 &self.config.urls,
943 0,
944 &topics,
945 mode,
946 self.config.operation_timeout,
947 )
948 .await?;
949 let (sender, receiver) = mpsc::channel(subscription_config.capacity);
950 let (shutdown, shutdown_receiver) = oneshot::channel();
951 tokio::spawn(run_subscription(
952 pubsub,
953 self.config.urls.clone(),
954 endpoint_index,
955 topics,
956 mode,
957 self.config.key_prefix.clone(),
958 self.config.operation_timeout,
959 subscription_config,
960 sender,
961 shutdown_receiver,
962 ));
963 Ok(RedisSubscription {
964 receiver,
965 shutdown: Some(shutdown),
966 })
967 }
968
969 async fn run<T>(
970 &self,
971 operation: &'static str,
972 future: impl Future<Output = redis::RedisResult<T>>,
973 ) -> Result<T, RedisStoreError> {
974 self.instrument(operation, future).await
975 }
976
977 async fn query<T: FromRedisValue>(&self, command: &mut Cmd) -> Result<T, RedisStoreError> {
978 let mut connection = self.connection().await?;
979 let operation = redis_command_name(command);
980 self.instrument(operation, command.query_async(&mut connection))
981 .await
982 }
983
984 async fn instrument<T>(
985 &self,
986 operation: &'static str,
987 future: impl Future<Output = redis::RedisResult<T>>,
988 ) -> Result<T, RedisStoreError> {
989 let started = Instant::now();
990 #[cfg(feature = "telemetry")]
991 let span = TelemetrySpan::start(
992 format!("redis.{operation}"),
993 TelemetrySpanKind::Client,
994 None,
995 [("db.operation.name", operation.to_owned())],
996 );
997 let result = match tokio::time::timeout(self.config.operation_timeout, future).await {
998 Ok(result) => result.map_err(RedisStoreError::Redis),
999 Err(_) => Err(RedisStoreError::Timeout),
1000 };
1001 if let Some(metrics) = &self.metrics {
1002 metrics.observe(operation, redis_outcome(&result), started.elapsed());
1003 }
1004 #[cfg(feature = "telemetry")]
1005 if let Err(error) = &result {
1006 span.set_error(error.to_string());
1007 }
1008 result
1009 }
1010
1011 async fn list_push<V: AsRef<[u8]>>(
1012 &self,
1013 name: &str,
1014 key: &str,
1015 values: &[V],
1016 ) -> Result<u64, RedisStoreError> {
1017 if values.is_empty() {
1018 return self.list_len(key).await;
1019 }
1020 let mut command = redis::cmd(name);
1021 command.arg(self.key(key));
1022 for value in values {
1023 command.arg(value.as_ref());
1024 }
1025 self.query(&mut command).await
1026 }
1027
1028 async fn set_members_command<V: AsRef<[u8]>>(
1029 &self,
1030 name: &str,
1031 key: &str,
1032 members: &[V],
1033 ) -> Result<u64, RedisStoreError> {
1034 if members.is_empty() {
1035 return Ok(0);
1036 }
1037 let mut command = redis::cmd(name);
1038 command.arg(self.key(key));
1039 for member in members {
1040 command.arg(member.as_ref());
1041 }
1042 self.query(&mut command).await
1043 }
1044
1045 fn key(&self, key: &str) -> String {
1046 format!("{}{}", self.config.key_prefix, key)
1047 }
1048}
1049
1050#[derive(Clone)]
1051enum RedisClient {
1052 Standalone(redis::Client),
1053 Cluster(ClusterClient),
1054}
1055
1056#[derive(Clone)]
1057enum RedisConnection {
1058 Standalone(MultiplexedConnection),
1059 Cluster(ClusterConnection),
1060}
1061
1062impl ConnectionLike for RedisConnection {
1063 fn req_packed_command<'a>(&'a mut self, command: &'a Cmd) -> RedisFuture<'a, Value> {
1064 match self {
1065 Self::Standalone(connection) => connection.req_packed_command(command),
1066 Self::Cluster(connection) => connection.req_packed_command(command),
1067 }
1068 }
1069
1070 fn req_packed_commands<'a>(
1071 &'a mut self,
1072 pipeline: &'a Pipeline,
1073 offset: usize,
1074 count: usize,
1075 ) -> RedisFuture<'a, Vec<Value>> {
1076 match self {
1077 Self::Standalone(connection) => connection.req_packed_commands(pipeline, offset, count),
1078 Self::Cluster(connection) => connection.req_packed_commands(pipeline, offset, count),
1079 }
1080 }
1081
1082 fn get_db(&self) -> i64 {
1083 match self {
1084 Self::Standalone(connection) => connection.get_db(),
1085 Self::Cluster(connection) => connection.get_db(),
1086 }
1087 }
1088}
1089
1090pub struct RedisLock {
1091 store: RedisStore,
1092 key: String,
1093 token: String,
1094 ttl: Duration,
1095 held: bool,
1096}
1097
1098#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1099pub enum RedisTtl {
1100 Missing,
1101 Persistent,
1102 ExpiresIn(Duration),
1103}
1104
1105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1107pub struct RedisModelCacheConfig {
1108 pub ttl: Duration,
1109 pub not_found_ttl: Duration,
1110 pub expiry_jitter: Duration,
1111 pub cache_not_found: bool,
1112}
1113
1114impl RedisModelCacheConfig {
1115 pub fn new(ttl: Duration) -> Self {
1116 assert!(!ttl.is_zero(), "model cache TTL must be positive");
1117 Self {
1118 ttl,
1119 not_found_ttl: Duration::from_secs(5),
1120 expiry_jitter: Duration::ZERO,
1121 cache_not_found: true,
1122 }
1123 }
1124
1125 pub fn with_not_found_ttl(mut self, ttl: Duration) -> Self {
1126 assert!(!ttl.is_zero(), "model cache not-found TTL must be positive");
1127 self.not_found_ttl = ttl;
1128 self
1129 }
1130
1131 pub fn with_expiry_jitter(mut self, jitter: Duration) -> Self {
1132 self.expiry_jitter = jitter;
1133 self
1134 }
1135
1136 pub fn with_cache_not_found(mut self, enabled: bool) -> Self {
1137 self.cache_not_found = enabled;
1138 self
1139 }
1140}
1141
1142#[derive(Serialize, serde::Deserialize)]
1143#[serde(tag = "state", content = "value", rename_all = "snake_case")]
1144enum ModelCacheEntry<T> {
1145 Value(T),
1146 NotFound,
1147}
1148
1149#[derive(Default)]
1150struct RedisModelCacheCounters {
1151 hits: AtomicU64,
1152 misses: AtomicU64,
1153 insertions: AtomicU64,
1154 evictions: AtomicU64,
1155}
1156
1157pub struct RedisModelCache<T, E> {
1164 store: RedisStore,
1165 config: RedisModelCacheConfig,
1166 flights: SingleFlight<String, Option<T>, RedisModelCacheError<E>>,
1167 counters: RedisModelCacheCounters,
1168 ttl_sequence: AtomicU64,
1169 marker: PhantomData<fn() -> E>,
1170}
1171
1172impl<T, E> RedisModelCache<T, E>
1173where
1174 T: Clone + Serialize + DeserializeOwned,
1175{
1176 pub fn new(store: RedisStore, config: RedisModelCacheConfig) -> Self {
1177 Self {
1178 store,
1179 config,
1180 flights: SingleFlight::new(),
1181 counters: RedisModelCacheCounters::default(),
1182 ttl_sequence: AtomicU64::new(0),
1183 marker: PhantomData,
1184 }
1185 }
1186
1187 pub async fn get_or_fetch<F, Fut>(
1189 &self,
1190 key: impl Into<String>,
1191 fetch: F,
1192 ) -> Result<Option<T>, SingleFlightError<RedisModelCacheError<E>>>
1193 where
1194 F: FnOnce() -> Fut,
1195 Fut: Future<Output = Result<Option<T>, E>>,
1196 {
1197 let key = key.into();
1198 self.flights
1199 .execute(key.clone(), || async {
1200 if let Some(bytes) = self
1201 .store
1202 .get(&key)
1203 .await
1204 .map_err(RedisModelCacheError::Store)?
1205 {
1206 let entry = serde_json::from_slice::<ModelCacheEntry<T>>(&bytes)
1207 .map_err(RedisModelCacheError::Serialization)?;
1208 self.counters.hits.fetch_add(1, Ordering::Relaxed);
1209 return Ok(match entry {
1210 ModelCacheEntry::Value(value) => Some(value),
1211 ModelCacheEntry::NotFound => None,
1212 });
1213 }
1214
1215 self.counters.misses.fetch_add(1, Ordering::Relaxed);
1216 let value = fetch().await.map_err(RedisModelCacheError::Fetch)?;
1217 match &value {
1218 Some(value) => {
1219 self.set_entry(&key, &ModelCacheEntry::Value(value), self.config.ttl)
1220 .await?;
1221 }
1222 None if self.config.cache_not_found => {
1223 self.set_entry::<T>(
1224 &key,
1225 &ModelCacheEntry::NotFound,
1226 self.config.not_found_ttl,
1227 )
1228 .await?;
1229 }
1230 None => {}
1231 }
1232 Ok(value)
1233 })
1234 .await
1235 }
1236
1237 pub async fn invalidate(&self, keys: &[&str]) -> Result<u64, RedisStoreError> {
1239 let mut removed = 0;
1240 for key in keys {
1241 removed += self.store.delete(&[*key]).await?;
1242 }
1243 self.counters
1244 .evictions
1245 .fetch_add(removed, Ordering::Relaxed);
1246 Ok(removed)
1247 }
1248
1249 pub fn stats(&self) -> CacheStats {
1250 CacheStats {
1251 hits: self.counters.hits.load(Ordering::Relaxed),
1252 misses: self.counters.misses.load(Ordering::Relaxed),
1253 insertions: self.counters.insertions.load(Ordering::Relaxed),
1254 evictions: self.counters.evictions.load(Ordering::Relaxed),
1255 }
1256 }
1257
1258 async fn set_entry<U: Serialize>(
1259 &self,
1260 key: &str,
1261 entry: &ModelCacheEntry<U>,
1262 base_ttl: Duration,
1263 ) -> Result<(), RedisModelCacheError<E>> {
1264 let bytes = serde_json::to_vec(entry).map_err(RedisModelCacheError::Serialization)?;
1265 let sequence = self.ttl_sequence.fetch_add(1, Ordering::Relaxed);
1266 let ttl = jittered_ttl(base_ttl, self.config.expiry_jitter, sequence);
1267 self.store
1268 .set(key, bytes, Some(ttl))
1269 .await
1270 .map_err(RedisModelCacheError::Store)?;
1271 self.counters.insertions.fetch_add(1, Ordering::Relaxed);
1272 Ok(())
1273 }
1274}
1275
1276#[derive(Debug)]
1277pub enum RedisModelCacheError<E> {
1278 Store(RedisStoreError),
1279 Serialization(serde_json::Error),
1280 Fetch(E),
1281}
1282
1283impl<E: fmt::Display> fmt::Display for RedisModelCacheError<E> {
1284 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1285 match self {
1286 Self::Store(error) => write!(formatter, "model cache store failed: {error}"),
1287 Self::Serialization(error) => {
1288 write!(formatter, "model cache serialization failed: {error}")
1289 }
1290 Self::Fetch(error) => write!(formatter, "model cache fetch failed: {error}"),
1291 }
1292 }
1293}
1294
1295impl<E: std::error::Error + 'static> std::error::Error for RedisModelCacheError<E> {}
1296
1297pub struct RedisJsonCache<T, E> {
1299 store: RedisStore,
1300 ttl: Duration,
1301 flights: SingleFlight<String, T, RedisCacheError<E>>,
1302 marker: PhantomData<fn() -> E>,
1303}
1304
1305impl<T, E> RedisJsonCache<T, E>
1306where
1307 T: Clone + Serialize + DeserializeOwned,
1308{
1309 pub fn new(store: RedisStore, ttl: Duration) -> Self {
1310 assert!(!ttl.is_zero(), "cache-aside TTL must be positive");
1311 Self {
1312 store,
1313 ttl,
1314 flights: SingleFlight::new(),
1315 marker: PhantomData,
1316 }
1317 }
1318
1319 pub async fn get_or_fetch<F, Fut>(
1320 &self,
1321 key: impl Into<String>,
1322 fetch: F,
1323 ) -> Result<T, SingleFlightError<RedisCacheError<E>>>
1324 where
1325 F: FnOnce() -> Fut,
1326 Fut: Future<Output = Result<T, E>>,
1327 {
1328 let key = key.into();
1329 self.flights
1330 .execute(key.clone(), || async {
1331 if let Some(value) = self
1332 .store
1333 .get_json(&key)
1334 .await
1335 .map_err(RedisCacheError::Store)?
1336 {
1337 return Ok(value);
1338 }
1339 let value = fetch().await.map_err(RedisCacheError::Fetch)?;
1340 self.store
1341 .set_json(&key, &value, Some(self.ttl))
1342 .await
1343 .map_err(RedisCacheError::Store)?;
1344 Ok(value)
1345 })
1346 .await
1347 }
1348
1349 pub async fn invalidate(&self, key: &str) -> Result<bool, RedisStoreError> {
1350 Ok(self.store.delete(&[key]).await? > 0)
1351 }
1352}
1353
1354#[derive(Debug)]
1355pub enum RedisCacheError<E> {
1356 Store(RedisStoreError),
1357 Fetch(E),
1358}
1359
1360impl<E: fmt::Display> fmt::Display for RedisCacheError<E> {
1361 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1362 match self {
1363 Self::Store(error) => write!(formatter, "cache store failed: {error}"),
1364 Self::Fetch(error) => write!(formatter, "cache fetch failed: {error}"),
1365 }
1366 }
1367}
1368
1369impl<E: std::error::Error + 'static> std::error::Error for RedisCacheError<E> {}
1370
1371impl RedisLock {
1372 pub async fn acquire(&mut self) -> Result<bool, RedisStoreError> {
1373 let mut connection = self.store.connection().await?;
1374 let result = self
1375 .store
1376 .run(
1377 "lock_acquire",
1378 redis::cmd("SET")
1379 .arg(self.store.key(&self.key))
1380 .arg(&self.token)
1381 .arg("NX")
1382 .arg("PX")
1383 .arg(duration_millis(self.ttl)?)
1384 .query_async::<Option<String>>(&mut connection),
1385 )
1386 .await?;
1387 self.held = result.is_some();
1388 Ok(self.held)
1389 }
1390
1391 pub async fn extend(&self, ttl: Duration) -> Result<bool, RedisStoreError> {
1392 if !self.held || ttl.is_zero() {
1393 return Ok(false);
1394 }
1395 let mut connection = self.store.connection().await?;
1396 let changed = self
1397 .store
1398 .run(
1399 "lock_extend",
1400 redis::Script::new(EXTEND_LOCK)
1401 .key(self.store.key(&self.key))
1402 .arg(&self.token)
1403 .arg(duration_millis(ttl)?)
1404 .invoke_async::<i64>(&mut connection),
1405 )
1406 .await?;
1407 Ok(changed == 1)
1408 }
1409
1410 pub async fn release(&mut self) -> Result<bool, RedisStoreError> {
1411 if !self.held {
1412 return Ok(false);
1413 }
1414 let mut connection = self.store.connection().await?;
1415 let deleted = self
1416 .store
1417 .run(
1418 "lock_release",
1419 redis::Script::new(RELEASE_LOCK)
1420 .key(self.store.key(&self.key))
1421 .arg(&self.token)
1422 .invoke_async::<i64>(&mut connection),
1423 )
1424 .await?;
1425 self.held = false;
1426 Ok(deleted == 1)
1427 }
1428
1429 pub fn is_held(&self) -> bool {
1430 self.held
1431 }
1432}
1433
1434#[derive(Debug)]
1435pub enum RedisStoreError {
1436 Redis(redis::RedisError),
1437 Json(serde_json::Error),
1438 Timeout,
1439 InvalidTtl,
1440 DurationOverflow,
1441 MissingEndpoint,
1442 InvalidArgument(&'static str),
1443 UnexpectedResponse(String),
1444}
1445
1446impl fmt::Display for RedisStoreError {
1447 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1448 match self {
1449 Self::Redis(error) => write!(formatter, "Redis operation failed: {error}"),
1450 Self::Json(error) => write!(formatter, "Redis JSON conversion failed: {error}"),
1451 Self::Timeout => formatter.write_str("Redis operation timed out"),
1452 Self::InvalidTtl => formatter.write_str("Redis TTL must be positive"),
1453 Self::DurationOverflow => {
1454 formatter.write_str("Redis duration exceeds u64 milliseconds")
1455 }
1456 Self::MissingEndpoint => formatter.write_str("Redis configuration has no endpoint"),
1457 Self::InvalidArgument(message) => formatter.write_str(message),
1458 Self::UnexpectedResponse(response) => {
1459 write!(
1460 formatter,
1461 "Redis returned an unexpected response: {response}"
1462 )
1463 }
1464 }
1465 }
1466}
1467
1468impl std::error::Error for RedisStoreError {}
1469
1470impl From<redis::RedisError> for RedisStoreError {
1471 fn from(error: redis::RedisError) -> Self {
1472 Self::Redis(error)
1473 }
1474}
1475
1476impl From<serde_json::Error> for RedisStoreError {
1477 fn from(error: serde_json::Error) -> Self {
1478 Self::Json(error)
1479 }
1480}
1481
1482fn redis_outcome<T>(result: &Result<T, RedisStoreError>) -> &'static str {
1483 match result {
1484 Ok(_) => "success",
1485 Err(RedisStoreError::Timeout) => "timeout",
1486 Err(_) => "error",
1487 }
1488}
1489
1490fn redis_command_name(command: &Cmd) -> &'static str {
1493 let packed = command.get_packed_command();
1494 let name = packed
1495 .split(|byte| *byte == b'\n')
1496 .nth(2)
1497 .map(|line| line.strip_suffix(b"\r").unwrap_or(line));
1498 match name {
1499 Some(b"DECRBY") => "decrement",
1500 Some(b"DEL") => "delete",
1501 Some(b"EVAL") => "eval",
1502 Some(b"EXISTS") => "exists",
1503 Some(b"GET") => "get",
1504 Some(b"HDEL") => "hash_delete",
1505 Some(b"HGET") => "hash_get",
1506 Some(b"HGETALL") => "hash_get_all",
1507 Some(b"HINCRBY") => "hash_increment",
1508 Some(b"HSET") => "hash_set",
1509 Some(b"INCRBY") => "increment",
1510 Some(b"LLEN") => "list_len",
1511 Some(b"LPOP") => "list_pop_left",
1512 Some(b"LPUSH") => "list_push_left",
1513 Some(b"LRANGE") => "list_range",
1514 Some(b"MGET") => "get_many",
1515 Some(b"MSET") => "set_many",
1516 Some(b"PERSIST") => "persist",
1517 Some(b"PEXPIRE") => "expire",
1518 Some(b"PING") => "ping",
1519 Some(b"PTTL") => "ttl",
1520 Some(b"PUBLISH") => "publish",
1521 Some(b"RPOP") => "list_pop_right",
1522 Some(b"RPUSH") => "list_push_right",
1523 Some(b"SADD") => "set_add",
1524 Some(b"SCARD") => "set_len",
1525 Some(b"SET") => "set",
1526 Some(b"SISMEMBER") => "set_contains",
1527 Some(b"SMEMBERS") => "set_members",
1528 Some(b"SREM") => "set_remove",
1529 Some(b"XACK") => "stream_ack",
1530 Some(b"XADD") => "stream_add",
1531 Some(b"XCLAIM") => "stream_claim",
1532 Some(b"XDEL") => "stream_delete",
1533 Some(b"XGROUP") => "stream_group",
1534 Some(b"XPENDING") => "stream_pending",
1535 Some(b"XREAD") => "stream_read",
1536 Some(b"XREADGROUP") => "stream_group_read",
1537 Some(b"ZADD") => "sorted_set_add",
1538 Some(b"ZCARD") => "sorted_set_len",
1539 Some(b"ZRANGE") => "sorted_set_range",
1540 Some(b"ZREM") => "sorted_set_remove",
1541 Some(b"ZSCORE") => "sorted_set_score",
1542 _ => "other",
1543 }
1544}
1545
1546async fn connect_subscription(
1547 urls: &[String],
1548 start_index: usize,
1549 topics: &[String],
1550 mode: RedisSubscriptionMode,
1551 timeout: Duration,
1552) -> Result<(PubSub, usize), RedisStoreError> {
1553 if urls.is_empty() {
1554 return Err(RedisStoreError::MissingEndpoint);
1555 }
1556 let mut last_error = None;
1557 for offset in 0..urls.len() {
1558 let index = (start_index + offset) % urls.len();
1559 let connection = async {
1560 let client = redis::Client::open(urls[index].as_str())?;
1561 let mut pubsub = client.get_async_pubsub().await?;
1562 match mode {
1563 RedisSubscriptionMode::Channels => pubsub.subscribe(topics).await?,
1564 RedisSubscriptionMode::Patterns => pubsub.psubscribe(topics).await?,
1565 }
1566 Ok::<_, redis::RedisError>(pubsub)
1567 };
1568 match tokio::time::timeout(timeout, connection).await {
1569 Ok(Ok(pubsub)) => return Ok((pubsub, index)),
1570 Ok(Err(error)) => last_error = Some(RedisStoreError::Redis(error)),
1571 Err(_) => last_error = Some(RedisStoreError::Timeout),
1572 }
1573 }
1574 Err(last_error.unwrap_or(RedisStoreError::MissingEndpoint))
1575}
1576
1577#[allow(clippy::too_many_arguments)]
1578async fn run_subscription(
1579 mut pubsub: PubSub,
1580 urls: Vec<String>,
1581 mut endpoint_index: usize,
1582 topics: Vec<String>,
1583 mode: RedisSubscriptionMode,
1584 key_prefix: String,
1585 operation_timeout: Duration,
1586 config: RedisSubscriptionConfig,
1587 sender: mpsc::Sender<RedisSubscriptionEvent>,
1588 mut shutdown: oneshot::Receiver<()>,
1589) {
1590 let mut dropped = 0_u64;
1591 loop {
1592 let disconnected = {
1593 let mut messages = pubsub.on_message();
1594 loop {
1595 tokio::select! {
1596 _ = &mut shutdown => {
1597 let _ = sender.send(RedisSubscriptionEvent::Closed).await;
1598 return;
1599 }
1600 _ = sender.closed() => return,
1601 message = messages.next() => match message {
1602 Some(message) => enqueue_subscription_message(
1603 &sender,
1604 subscription_message(message, &key_prefix),
1605 &mut dropped,
1606 ),
1607 None => break "Redis Pub/Sub connection closed".to_owned(),
1608 }
1609 }
1610 }
1611 };
1612
1613 if dropped > 0 {
1614 let event = RedisSubscriptionEvent::Lagged { dropped };
1615 if !send_subscription_event(&sender, event, &mut shutdown).await {
1616 return;
1617 }
1618 dropped = 0;
1619 }
1620
1621 let mut delay = config.reconnect_delay;
1622 let mut error = disconnected;
1623 loop {
1624 if !send_subscription_event(
1625 &sender,
1626 RedisSubscriptionEvent::Disconnected {
1627 error,
1628 retry_in: delay,
1629 },
1630 &mut shutdown,
1631 )
1632 .await
1633 {
1634 return;
1635 }
1636 tokio::select! {
1637 _ = &mut shutdown => {
1638 let _ = sender.send(RedisSubscriptionEvent::Closed).await;
1639 return;
1640 }
1641 _ = sender.closed() => return,
1642 _ = tokio::time::sleep(delay) => {}
1643 }
1644
1645 let start_index = (endpoint_index + 1) % urls.len();
1646 let reconnect =
1647 connect_subscription(&urls, start_index, &topics, mode, operation_timeout);
1648 let result = tokio::select! {
1649 _ = &mut shutdown => {
1650 let _ = sender.send(RedisSubscriptionEvent::Closed).await;
1651 return;
1652 }
1653 _ = sender.closed() => return,
1654 result = reconnect => result,
1655 };
1656 match result {
1657 Ok((connection, index)) => {
1658 pubsub = connection;
1659 endpoint_index = index;
1660 if !send_subscription_event(
1661 &sender,
1662 RedisSubscriptionEvent::Reconnected,
1663 &mut shutdown,
1664 )
1665 .await
1666 {
1667 return;
1668 }
1669 break;
1670 }
1671 Err(reconnect_error) => {
1672 error = reconnect_error.to_string();
1673 delay = delay.saturating_mul(2).min(config.max_reconnect_delay);
1674 }
1675 }
1676 }
1677 }
1678}
1679
1680fn subscription_message(message: redis::Msg, key_prefix: &str) -> RedisSubscriptionMessage {
1681 let channel = message
1682 .get_channel_name()
1683 .strip_prefix(key_prefix)
1684 .unwrap_or_else(|| message.get_channel_name())
1685 .to_owned();
1686 let pattern = message
1687 .get_pattern::<Option<String>>()
1688 .unwrap_or_default()
1689 .map(|pattern| {
1690 pattern
1691 .strip_prefix(key_prefix)
1692 .unwrap_or(&pattern)
1693 .to_owned()
1694 });
1695 RedisSubscriptionMessage {
1696 channel,
1697 pattern,
1698 payload: message.get_payload_bytes().to_vec(),
1699 }
1700}
1701
1702fn enqueue_subscription_message(
1703 sender: &mpsc::Sender<RedisSubscriptionEvent>,
1704 message: RedisSubscriptionMessage,
1705 dropped: &mut u64,
1706) {
1707 if *dropped > 0 {
1708 if sender
1709 .try_send(RedisSubscriptionEvent::Lagged { dropped: *dropped })
1710 .is_ok()
1711 {
1712 *dropped = 0;
1713 } else {
1714 *dropped = dropped.saturating_add(1);
1715 return;
1716 }
1717 }
1718 if sender
1719 .try_send(RedisSubscriptionEvent::Message(message))
1720 .is_err()
1721 {
1722 *dropped = dropped.saturating_add(1);
1723 }
1724}
1725
1726async fn send_subscription_event(
1727 sender: &mpsc::Sender<RedisSubscriptionEvent>,
1728 event: RedisSubscriptionEvent,
1729 shutdown: &mut oneshot::Receiver<()>,
1730) -> bool {
1731 tokio::select! {
1732 _ = shutdown => {
1733 let _ = sender.send(RedisSubscriptionEvent::Closed).await;
1734 false
1735 }
1736 result = sender.send(event) => result.is_ok(),
1737 }
1738}
1739
1740fn duration_millis(duration: Duration) -> Result<u64, RedisStoreError> {
1741 duration
1742 .as_millis()
1743 .try_into()
1744 .map_err(|_| RedisStoreError::DurationOverflow)
1745}
1746
1747fn append_stream_read_options(
1748 command: &mut Cmd,
1749 count: Option<usize>,
1750 block: Option<Duration>,
1751 no_ack: bool,
1752) -> Result<(), RedisStoreError> {
1753 if count == Some(0) {
1754 return Err(RedisStoreError::InvalidArgument(
1755 "Redis stream read count must be positive",
1756 ));
1757 }
1758 if let Some(count) = count {
1759 command.arg("COUNT").arg(count);
1760 }
1761 if let Some(block) = block {
1762 command.arg("BLOCK").arg(duration_millis(block)?);
1763 }
1764 if no_ack {
1765 command.arg("NOACK");
1766 }
1767 Ok(())
1768}
1769
1770fn append_streams(
1771 command: &mut Cmd,
1772 streams: &[(&str, &str)],
1773 key: impl Fn(&str) -> String,
1774) -> Result<(), RedisStoreError> {
1775 if streams.is_empty() {
1776 return Err(RedisStoreError::InvalidArgument(
1777 "Redis stream reads require at least one stream",
1778 ));
1779 }
1780 command.arg("STREAMS");
1781 for (stream, _) in streams {
1782 command.arg(key(stream));
1783 }
1784 for (_, id) in streams {
1785 command.arg(id);
1786 }
1787 Ok(())
1788}
1789
1790fn expect_ok(response: String, operation: &str) -> Result<(), RedisStoreError> {
1791 if response == "OK" {
1792 Ok(())
1793 } else {
1794 Err(RedisStoreError::UnexpectedResponse(format!(
1795 "{operation} returned {response}"
1796 )))
1797 }
1798}
1799
1800fn unique_token() -> String {
1801 let timestamp = SystemTime::now()
1802 .duration_since(UNIX_EPOCH)
1803 .unwrap_or_default()
1804 .as_nanos();
1805 let sequence = TOKEN_COUNTER.fetch_add(1, Ordering::Relaxed);
1806 format!("{}-{timestamp}-{sequence}", std::process::id())
1807}
1808
1809#[cfg(test)]
1810mod tests {
1811 use super::*;
1812 use serde::{Deserialize, Serialize};
1813
1814 #[tokio::test]
1815 async fn operation_hooks_emit_bounded_success_and_error_metrics() {
1816 let registry = Metrics::new();
1817 let metrics = RedisStoreMetrics::register(®istry).unwrap();
1818 let store = RedisStore::new(RedisStoreConfig::new("redis://127.0.0.1/"))
1819 .unwrap()
1820 .with_metrics(metrics.clone());
1821
1822 store
1823 .instrument("get", async { Ok::<_, redis::RedisError>(()) })
1824 .await
1825 .unwrap();
1826 let error = store
1827 .instrument("get", async {
1828 Err::<(), _>(redis::RedisError::from((
1829 redis::ErrorKind::TypeError,
1830 "test failure",
1831 )))
1832 })
1833 .await
1834 .unwrap_err();
1835 assert!(matches!(error, RedisStoreError::Redis(_)));
1836
1837 let mut known = redis::cmd("GET");
1838 known.arg("key");
1839 assert_eq!(redis_command_name(&known), "get");
1840 let mut unknown = redis::cmd("APPLICATION_PRIVATE_COMMAND");
1841 unknown.arg("key");
1842 assert_eq!(redis_command_name(&unknown), "other");
1843
1844 let timed = RedisStore::new(
1845 RedisStoreConfig::new("redis://127.0.0.1/")
1846 .with_operation_timeout(Duration::from_millis(1)),
1847 )
1848 .unwrap()
1849 .with_metrics(metrics);
1850 assert!(matches!(
1851 timed
1852 .instrument("get", std::future::pending::<redis::RedisResult<()>>())
1853 .await,
1854 Err(RedisStoreError::Timeout)
1855 ));
1856
1857 let rendered = registry.render();
1858 assert!(rendered
1859 .contains("rust_zero_redis_operations_total{operation=\"get\",outcome=\"success\"} 1"));
1860 assert!(rendered
1861 .contains("rust_zero_redis_operations_total{operation=\"get\",outcome=\"error\"} 1"));
1862 assert!(rendered
1863 .contains("rust_zero_redis_operations_total{operation=\"get\",outcome=\"timeout\"} 1"));
1864 }
1865
1866 #[test]
1867 fn prefixes_keys_and_creates_unique_lock_tokens() {
1868 let store =
1869 RedisStore::new(RedisStoreConfig::new("redis://127.0.0.1/").with_key_prefix("test:"))
1870 .unwrap();
1871 assert_eq!(store.key("users"), "test:users");
1872 assert_eq!(store.prefixed_key("users"), "test:users");
1873 assert_ne!(
1874 store.lock("lock", Duration::from_secs(1)).token,
1875 store.lock("lock", Duration::from_secs(1)).token
1876 );
1877 }
1878
1879 #[test]
1880 fn rejects_zero_ttl_without_connecting() {
1881 let store = RedisStore::new(RedisStoreConfig::new("redis://127.0.0.1/")).unwrap();
1882 let lock = store.lock("valid", Duration::from_secs(1));
1883 assert!(!lock.held);
1884 }
1885
1886 #[test]
1887 fn builds_cluster_from_multiple_seed_nodes() {
1888 let config =
1889 RedisStoreConfig::cluster(["redis://127.0.0.1:7000/", "redis://127.0.0.1:7001/"])
1890 .with_key_prefix("cluster:");
1891 let store = RedisStore::new(config).unwrap();
1892 assert!(matches!(store.client, RedisClient::Cluster(_)));
1893 assert_eq!(store.key("{user:7}:profile"), "cluster:{user:7}:profile");
1894 }
1895
1896 #[test]
1897 fn rejects_an_empty_cluster_seed_list() {
1898 let error = RedisStore::new(RedisStoreConfig::cluster(Vec::<String>::new()))
1899 .err()
1900 .expect("empty cluster configuration must fail");
1901 assert!(matches!(error, RedisStoreError::Redis(_)));
1902 }
1903
1904 #[test]
1905 fn configures_model_cache_expiry_policy() {
1906 let config = RedisModelCacheConfig::new(Duration::from_secs(60))
1907 .with_not_found_ttl(Duration::from_secs(3))
1908 .with_expiry_jitter(Duration::from_secs(7))
1909 .with_cache_not_found(false);
1910 assert_eq!(config.ttl, Duration::from_secs(60));
1911 assert_eq!(config.not_found_ttl, Duration::from_secs(3));
1912 assert_eq!(config.expiry_jitter, Duration::from_secs(7));
1913 assert!(!config.cache_not_found);
1914 }
1915
1916 #[tokio::test]
1917 async fn rejects_invalid_stream_operations_before_connecting() {
1918 let store = RedisStore::new(RedisStoreConfig::new("redis://127.0.0.1/")).unwrap();
1919 assert!(matches!(
1920 store.stream_add::<&[u8]>("events", None, &[]).await,
1921 Err(RedisStoreError::InvalidArgument(_))
1922 ));
1923 assert!(matches!(
1924 store.stream_read(&[], None, None).await,
1925 Err(RedisStoreError::InvalidArgument(_))
1926 ));
1927 assert!(matches!(
1928 store
1929 .stream_group_read("", "worker", &[("events", ">")], None, None, false)
1930 .await,
1931 Err(RedisStoreError::InvalidArgument(_))
1932 ));
1933 assert!(matches!(
1934 store
1935 .stream_claim("events", "workers", "worker", Duration::ZERO, &[])
1936 .await,
1937 Err(RedisStoreError::InvalidArgument(_))
1938 ));
1939 }
1940
1941 #[tokio::test]
1942 async fn rejects_invalid_subscription_configuration_before_connecting() {
1943 let store = RedisStore::new(RedisStoreConfig::new("redis://127.0.0.1/")).unwrap();
1944 assert!(matches!(
1945 store
1946 .subscribe(Vec::<String>::new(), RedisSubscriptionConfig::default())
1947 .await,
1948 Err(RedisStoreError::InvalidArgument(_))
1949 ));
1950 assert!(matches!(
1951 store
1952 .psubscribe(
1953 ["events:*"],
1954 RedisSubscriptionConfig::default().with_capacity(1),
1955 )
1956 .await,
1957 Err(RedisStoreError::InvalidArgument(_))
1958 ));
1959 assert!(matches!(
1960 store
1961 .subscribe(
1962 ["events"],
1963 RedisSubscriptionConfig::default()
1964 .with_reconnect_delay(Duration::from_secs(2))
1965 .with_max_reconnect_delay(Duration::from_secs(1)),
1966 )
1967 .await,
1968 Err(RedisStoreError::InvalidArgument(_))
1969 ));
1970 }
1971
1972 #[tokio::test]
1973 async fn bounded_subscription_delivery_reports_lag() {
1974 let (sender, mut receiver) = mpsc::channel(2);
1975 let message = |payload: u8| RedisSubscriptionMessage {
1976 channel: "events".to_owned(),
1977 pattern: None,
1978 payload: vec![payload],
1979 };
1980 let mut dropped = 0;
1981 enqueue_subscription_message(&sender, message(1), &mut dropped);
1982 enqueue_subscription_message(&sender, message(2), &mut dropped);
1983 enqueue_subscription_message(&sender, message(3), &mut dropped);
1984 assert_eq!(dropped, 1);
1985 assert!(matches!(
1986 receiver.recv().await,
1987 Some(RedisSubscriptionEvent::Message(message)) if message.payload == [1]
1988 ));
1989 assert!(matches!(
1990 receiver.recv().await,
1991 Some(RedisSubscriptionEvent::Message(message)) if message.payload == [2]
1992 ));
1993 enqueue_subscription_message(&sender, message(4), &mut dropped);
1994 assert_eq!(dropped, 0);
1995 assert!(matches!(
1996 receiver.recv().await,
1997 Some(RedisSubscriptionEvent::Lagged { dropped: 1 })
1998 ));
1999 assert!(matches!(
2000 receiver.recv().await,
2001 Some(RedisSubscriptionEvent::Message(message)) if message.payload == [4]
2002 ));
2003 }
2004
2005 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2006 struct User {
2007 id: u64,
2008 name: String,
2009 }
2010
2011 #[tokio::test]
2012 async fn redis_integration_covers_json_counters_locks_and_cache_aside() {
2013 let Ok(url) = std::env::var("RUST_ZERO_REDIS_URL") else {
2014 return;
2015 };
2016 let namespace = format!("rust-zero:{}:", std::process::id());
2017 let store =
2018 RedisStore::new(RedisStoreConfig::new(url.clone()).with_key_prefix(namespace.clone()))
2019 .unwrap();
2020 let peer_store =
2021 RedisStore::new(RedisStoreConfig::new(url).with_key_prefix(namespace)).unwrap();
2022 store.ping().await.unwrap();
2023 let mut channel_subscription = store
2024 .subscribe(["events:created"], RedisSubscriptionConfig::default())
2025 .await
2026 .unwrap();
2027 let mut pattern_subscription = store
2028 .psubscribe(["events:*"], RedisSubscriptionConfig::default())
2029 .await
2030 .unwrap();
2031 assert_eq!(store.publish("events:created", b"user-7").await.unwrap(), 2);
2032 let channel_event =
2033 tokio::time::timeout(Duration::from_secs(2), channel_subscription.recv())
2034 .await
2035 .unwrap()
2036 .unwrap();
2037 assert!(matches!(
2038 channel_event,
2039 RedisSubscriptionEvent::Message(RedisSubscriptionMessage {
2040 channel,
2041 pattern: None,
2042 payload,
2043 }) if channel == "events:created" && payload == b"user-7"
2044 ));
2045 let pattern_event =
2046 tokio::time::timeout(Duration::from_secs(2), pattern_subscription.recv())
2047 .await
2048 .unwrap()
2049 .unwrap();
2050 assert!(matches!(
2051 pattern_event,
2052 RedisSubscriptionEvent::Message(RedisSubscriptionMessage {
2053 channel,
2054 pattern: Some(pattern),
2055 payload,
2056 }) if channel == "events:created" && pattern == "events:*" && payload == b"user-7"
2057 ));
2058 channel_subscription.shutdown();
2059 pattern_subscription.shutdown();
2060 assert_eq!(
2061 channel_subscription.recv().await,
2062 Some(RedisSubscriptionEvent::Closed)
2063 );
2064 assert_eq!(
2065 pattern_subscription.recv().await,
2066 Some(RedisSubscriptionEvent::Closed)
2067 );
2068 store
2069 .delete(&[
2070 "user", "count", "lock", "string", "hash", "list", "set", "sorted", "stream",
2071 "pipeline",
2072 ])
2073 .await
2074 .unwrap();
2075
2076 let user = User {
2077 id: 7,
2078 name: "Ada".to_owned(),
2079 };
2080 store
2081 .set_json("user", &user, Some(Duration::from_secs(10)))
2082 .await
2083 .unwrap();
2084 assert_eq!(store.get_json::<User>("user").await.unwrap(), Some(user));
2085 assert_eq!(store.increment("count", 2).await.unwrap(), 2);
2086 assert_eq!(store.decrement("count", 1).await.unwrap(), 1);
2087 let raw_count: i64 = store
2088 .do_command(
2089 redis::cmd("INCRBY")
2090 .arg(store.prefixed_key("count"))
2091 .arg(4)
2092 .to_owned(),
2093 )
2094 .await
2095 .unwrap();
2096 assert_eq!(raw_count, 5);
2097
2098 let mut pipeline = redis::pipe();
2099 pipeline
2100 .cmd("SET")
2101 .arg(store.prefixed_key("pipeline"))
2102 .arg("batched")
2103 .ignore()
2104 .cmd("GET")
2105 .arg(store.prefixed_key("pipeline"));
2106 let (pipeline_value,): (String,) = store.do_pipeline(&pipeline).await.unwrap();
2107 assert_eq!(pipeline_value, "batched");
2108 let no_arguments: [&[u8]; 0] = [];
2109 let scripted: String = store
2110 .eval(
2111 "return redis.call('GET', KEYS[1])",
2112 &["pipeline"],
2113 &no_arguments,
2114 )
2115 .await
2116 .unwrap();
2117 assert_eq!(scripted, "batched");
2118
2119 store
2120 .stream_group_create("stream", "workers", "0", true)
2121 .await
2122 .unwrap();
2123 store
2124 .stream_group_set_id("stream", "workers", "$")
2125 .await
2126 .unwrap();
2127 let stream_id = store
2128 .stream_add("stream", None, &[("kind", "created"), ("id", "7")])
2129 .await
2130 .unwrap();
2131 let delivered = store
2132 .stream_group_read(
2133 "workers",
2134 "worker-1",
2135 &[("stream", ">")],
2136 Some(10),
2137 None,
2138 false,
2139 )
2140 .await
2141 .unwrap();
2142 assert_eq!(delivered.keys.len(), 1);
2143 assert_eq!(delivered.keys[0].ids[0].id, stream_id);
2144 assert!(!matches!(
2145 store.stream_pending("stream", "workers").await.unwrap(),
2146 Value::Nil
2147 ));
2148 let claimed = store
2149 .stream_claim(
2150 "stream",
2151 "workers",
2152 "worker-2",
2153 Duration::ZERO,
2154 &[&stream_id],
2155 )
2156 .await
2157 .unwrap();
2158 assert!(!matches!(claimed, Value::Nil));
2159 assert_eq!(
2160 store
2161 .stream_ack("stream", "workers", &[&stream_id])
2162 .await
2163 .unwrap(),
2164 1
2165 );
2166 assert_eq!(
2167 store.stream_delete("stream", &[&stream_id]).await.unwrap(),
2168 1
2169 );
2170 assert!(store
2171 .stream_group_destroy("stream", "workers")
2172 .await
2173 .unwrap());
2174
2175 assert!(store
2176 .set_if_absent("string", "first", Some(Duration::from_secs(10)))
2177 .await
2178 .unwrap());
2179 assert!(!store.set_if_absent("string", "second", None).await.unwrap());
2180 assert_eq!(
2181 store.get_string("string").await.unwrap().as_deref(),
2182 Some("first")
2183 );
2184 assert!(matches!(
2185 store.ttl("string").await.unwrap(),
2186 RedisTtl::ExpiresIn(_)
2187 ));
2188 assert!(store.persist("string").await.unwrap());
2189 assert_eq!(store.ttl("string").await.unwrap(), RedisTtl::Persistent);
2190
2191 assert!(store.hash_set("hash", "name", "Ada").await.unwrap());
2192 assert_eq!(
2193 store.hash_get("hash", "name").await.unwrap().as_deref(),
2194 Some(b"Ada".as_slice())
2195 );
2196 assert_eq!(store.hash_increment("hash", "visits", 2).await.unwrap(), 2);
2197 assert_eq!(store.hash_get_all("hash").await.unwrap().len(), 2);
2198
2199 assert_eq!(
2200 store.list_push_back("list", &["one", "two"]).await.unwrap(),
2201 2
2202 );
2203 assert_eq!(
2204 store.list_range("list", 0, -1).await.unwrap(),
2205 vec![b"one".to_vec(), b"two".to_vec()]
2206 );
2207 assert_eq!(
2208 store.list_pop_front("list").await.unwrap(),
2209 Some(b"one".to_vec())
2210 );
2211
2212 assert_eq!(store.set_add("set", &["one", "two"]).await.unwrap(), 2);
2213 assert!(store.set_contains("set", "two").await.unwrap());
2214 assert_eq!(store.set_len("set").await.unwrap(), 2);
2215
2216 assert!(store.sorted_set_add("sorted", 2.0, "two").await.unwrap());
2217 assert!(store.sorted_set_add("sorted", 1.0, "one").await.unwrap());
2218 assert_eq!(
2219 store
2220 .sorted_set_range_with_scores("sorted", 0, -1)
2221 .await
2222 .unwrap(),
2223 vec![(b"one".to_vec(), 1.0), (b"two".to_vec(), 2.0)]
2224 );
2225
2226 let mut owner = store.lock("lock", Duration::from_secs(5));
2227 let mut contender = store.lock("lock", Duration::from_secs(5));
2228 assert!(owner.acquire().await.unwrap());
2229 assert!(!contender.acquire().await.unwrap());
2230 assert!(owner.extend(Duration::from_secs(10)).await.unwrap());
2231 assert!(owner.release().await.unwrap());
2232 assert!(contender.acquire().await.unwrap());
2233 assert!(contender.release().await.unwrap());
2234
2235 let cache = RedisJsonCache::<User, String>::new(store.clone(), Duration::from_secs(10));
2236 cache.invalidate("cached-user").await.unwrap();
2237 let cached = cache
2238 .get_or_fetch("cached-user", || async {
2239 Ok(User {
2240 id: 8,
2241 name: "Grace".to_owned(),
2242 })
2243 })
2244 .await
2245 .unwrap();
2246 assert_eq!(cached.name, "Grace");
2247 let reused = cache
2248 .get_or_fetch("cached-user", || async { Err("must not fetch".to_owned()) })
2249 .await
2250 .unwrap();
2251 assert_eq!(reused, cached);
2252
2253 let model_config = RedisModelCacheConfig::new(Duration::from_secs(10))
2254 .with_not_found_ttl(Duration::from_secs(2))
2255 .with_expiry_jitter(Duration::from_secs(1));
2256 let model_a = RedisModelCache::<User, String>::new(store.clone(), model_config);
2257 let model_b = RedisModelCache::<User, String>::new(peer_store, model_config);
2258 model_a
2259 .invalidate(&["model-user", "missing-model", "broken-model"])
2260 .await
2261 .unwrap();
2262
2263 store
2264 .set("broken-model", b"not-json", Some(Duration::from_secs(10)))
2265 .await
2266 .unwrap();
2267 let malformed = model_a
2268 .get_or_fetch("broken-model", || async { Ok(None) })
2269 .await
2270 .unwrap_err();
2271 assert!(matches!(
2272 malformed,
2273 SingleFlightError::Operation(error)
2274 if matches!(error.as_ref(), RedisModelCacheError::Serialization(_))
2275 ));
2276
2277 let first = model_a
2278 .get_or_fetch("model-user", || async {
2279 Ok(Some(User {
2280 id: 10,
2281 name: "Shared".to_owned(),
2282 }))
2283 })
2284 .await
2285 .unwrap();
2286 assert_eq!(
2287 first.as_ref().map(|user| user.name.as_str()),
2288 Some("Shared")
2289 );
2290 let shared = model_b
2291 .get_or_fetch("model-user", || async {
2292 Err("must use shared cache".to_owned())
2293 })
2294 .await
2295 .unwrap();
2296 assert_eq!(shared, first);
2297
2298 assert_eq!(
2299 model_a
2300 .get_or_fetch("missing-model", || async { Ok(None) })
2301 .await
2302 .unwrap(),
2303 None
2304 );
2305 assert_eq!(
2306 model_b
2307 .get_or_fetch("missing-model", || async {
2308 Err("must use not-found sentinel".to_owned())
2309 })
2310 .await
2311 .unwrap(),
2312 None
2313 );
2314
2315 assert_eq!(model_a.invalidate(&["model-user"]).await.unwrap(), 1);
2316 let refreshed = model_b
2317 .get_or_fetch("model-user", || async {
2318 Ok(Some(User {
2319 id: 10,
2320 name: "Refreshed".to_owned(),
2321 }))
2322 })
2323 .await
2324 .unwrap();
2325 assert_eq!(
2326 refreshed.as_ref().map(|user| user.name.as_str()),
2327 Some("Refreshed")
2328 );
2329 assert_eq!(model_a.stats().misses, 2);
2330 assert_eq!(model_b.stats().hits, 2);
2331 }
2332
2333 #[tokio::test]
2334 async fn redis_cluster_integration_routes_across_seed_nodes() {
2335 let Ok(nodes) = std::env::var("RUST_ZERO_REDIS_CLUSTER_URLS") else {
2336 return;
2337 };
2338 let store = RedisStore::new(
2339 RedisStoreConfig::cluster(
2340 nodes
2341 .split(',')
2342 .map(str::trim)
2343 .filter(|node| !node.is_empty()),
2344 )
2345 .with_key_prefix(format!("rust-zero-cluster:{}:", std::process::id())),
2346 )
2347 .unwrap();
2348
2349 store.ping().await.unwrap();
2350 store
2351 .set_many(&[("{one}:value", "one"), ("{two}:value", "two")])
2352 .await
2353 .unwrap();
2354 assert_eq!(
2355 store
2356 .get_many(&["{one}:value", "{two}:value"])
2357 .await
2358 .unwrap(),
2359 vec![Some(b"one".to_vec()), Some(b"two".to_vec())]
2360 );
2361 let user = User {
2362 id: 9,
2363 name: "Cluster".to_owned(),
2364 };
2365 store
2366 .set_json("{user:9}:json", &user, Some(Duration::from_secs(10)))
2367 .await
2368 .unwrap();
2369 assert_eq!(
2370 store.get_json::<User>("{user:9}:json").await.unwrap(),
2371 Some(user)
2372 );
2373 let mut lock = store.lock("{user:9}:lock", Duration::from_secs(5));
2374 assert!(lock.acquire().await.unwrap());
2375 assert!(lock.extend(Duration::from_secs(10)).await.unwrap());
2376 assert!(lock.release().await.unwrap());
2377 assert_eq!(
2378 store
2379 .delete(&[
2380 "{one}:value",
2381 "{two}:value",
2382 "{user:9}:json",
2383 "{user:9}:lock",
2384 ])
2385 .await
2386 .unwrap(),
2387 3
2388 );
2389
2390 let model = RedisModelCache::<User, String>::new(
2391 store.clone(),
2392 RedisModelCacheConfig::new(Duration::from_secs(10)),
2393 );
2394 let key = "{user:11}:model";
2395 model.invalidate(&[key]).await.unwrap();
2396 let cached = model
2397 .get_or_fetch(key, || async {
2398 Ok(Some(User {
2399 id: 11,
2400 name: "Cluster model".to_owned(),
2401 }))
2402 })
2403 .await
2404 .unwrap();
2405 assert_eq!(cached.as_ref().map(|user| user.id), Some(11));
2406 assert_eq!(model.invalidate(&[key]).await.unwrap(), 1);
2407 }
2408}