Skip to main content

rust_zero_core/
stores_redis.rs

1//! Redis data structures, locks, streams, subscriptions, and model caching.
2//!
3//! ```no_run
4//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
5//! use rust_zero_core::{RedisStore, RedisStoreConfig};
6//! let redis = RedisStore::new(RedisStoreConfig::new("redis://127.0.0.1/"))?;
7//! redis.ping().await?;
8//! # Ok(())
9//! # }
10//! ```
11
12use 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/// Cardinality-bounded Redis command metrics installed on [`RedisStore`].
101#[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/// Delivery and reconnect policy for a Redis Pub/Sub subscription.
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub struct RedisSubscriptionConfig {
145    /// Maximum number of events waiting for the consumer.
146    pub capacity: usize,
147    /// Delay before the first reconnect attempt.
148    pub reconnect_delay: Duration,
149    /// Upper bound for exponential reconnect backoff.
150    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/// One binary-safe message received from a channel or pattern subscription.
195#[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/// Observable subscription state and delivery events.
203#[derive(Debug, Clone, PartialEq, Eq)]
204pub enum RedisSubscriptionEvent {
205    Message(RedisSubscriptionMessage),
206    /// Messages were discarded because the bounded consumer queue was full.
207    Lagged {
208        dropped: u64,
209    },
210    /// The connection ended or a reconnect attempt failed and will be retried.
211    Disconnected {
212        error: String,
213        retry_in: Duration,
214    },
215    /// All requested channels or patterns were restored on a new connection.
216    Reconnected,
217    /// The subscription was explicitly shut down.
218    Closed,
219}
220
221/// Receiver for a reconnecting Redis channel or pattern subscription.
222pub 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    /// Requests shutdown. A final [`RedisSubscriptionEvent::Closed`] is delivered before EOF.
237    pub fn shutdown(&mut self) {
238        if let Some(shutdown) = self.shutdown.take() {
239            let _ = shutdown.send(());
240        }
241    }
242
243    /// Shuts down and waits until the worker has stopped.
244    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    /// Installs metrics for commands executed by this store.
286    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    /// Executes an arbitrary Redis command using this store's connection and timeout policy.
302    ///
303    /// Arguments are passed through exactly as provided. In particular, keys in a raw command are
304    /// not automatically namespaced with [`RedisStoreConfig::key_prefix`]; callers can use
305    /// [`Self::prefixed_key`] when constructing commands that should share the store namespace.
306    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    /// Applies this store's configured namespace to a key for use with [`Self::do_command`].
314    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    /// Subscribes to exact channel names using a reconnecting, bounded receiver.
667    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    /// Subscribes to Redis channel patterns using a reconnecting, bounded receiver.
681    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    /// Executes a caller-built Redis pipeline using the shared connection and operation timeout.
695    ///
696    /// Like [`Self::do_command`], pipeline keys are passed through unchanged. Use
697    /// [`Self::prefixed_key`] when adding keys that belong to this store's namespace.
698    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    /// Evaluates a Lua script with automatically prefixed keys and binary-safe arguments.
708    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    /// Appends a field/value entry to a Redis stream and returns its generated or explicit ID.
726    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    /// Reads entries from one or more streams using `XREAD`.
746    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    /// Creates a consumer group at `id`, optionally creating an empty stream first.
759    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    /// Destroys a consumer group and reports whether it existed.
775    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    /// Reads stream entries as a consumer group member using `XREADGROUP`.
786    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    /// Acknowledges delivered stream entries and returns the number newly acknowledged.
808    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    /// Returns the raw `XPENDING` summary so callers retain Redis-version-specific fields.
823    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    /// Claims pending entries for another consumer using `XCLAIM`.
830    ///
831    /// The response remains a raw Redis value because newer Redis versions may add optional
832    /// fields while retaining wire compatibility.
833    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    /// Deletes stream entries and returns the number removed.
857    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    /// Moves a Redis stream consumer group's last-delivered cursor.
867    ///
868    /// `id` accepts Redis stream IDs as well as the special `$` and `0` values. This corresponds
869    /// to `XGROUP SETID`, including the helper added by go-zero v1.10.3.
870    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/// Expiry and not-found policy for a Redis-backed model cache.
1106#[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
1157/// Redis-backed positive and not-found model caching with per-process miss coalescing.
1158///
1159/// Cache entries use a tagged JSON representation, so a cached not-found result cannot be
1160/// confused with a missing Redis key. Separate instances that use the same [`RedisStore`]
1161/// namespace observe writes and invalidations immediately, which supports cross-process model
1162/// mutation invalidation without an additional in-memory coherence layer.
1163pub 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    /// Returns a cached model, a cached not-found result, or invokes `fetch` on a cache miss.
1188    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    /// Deletes all supplied model/index keys, routing each key independently for Redis Cluster.
1238    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
1297/// JSON cache-aside access backed by Redis with per-key miss coalescing.
1298pub 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
1490/// Extracts a bounded command label from Redis' packed RESP representation.
1491/// Unknown application-supplied commands collapse to `other` to prevent metric-cardinality leaks.
1492fn 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(&registry).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}