1use async_trait::async_trait;
58use redis::{
59 aio::ConnectionManager, AsyncCommands, Client, RedisResult, Script,
60};
61use serde::{Deserialize, Serialize};
62use std::sync::Arc;
63use std::time::Duration;
64use tokio::sync::RwLock;
65use tracing::{debug, error, trace, warn};
66
67use super::backend::StateBackend;
68use crate::error::{StateError, StateResult};
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct RedisConfig {
73 pub urls: Vec<String>,
75 pub key_prefix: String,
77 pub default_ttl: Option<Duration>,
79 pub min_connections: u32,
81 pub max_connections: u32,
83 pub connect_timeout: Duration,
85 pub read_timeout: Duration,
87 pub write_timeout: Duration,
89 pub max_retries: u32,
91 pub retry_base_delay: Duration,
93 pub retry_max_delay: Duration,
95 pub cluster_mode: bool,
97 pub tls_enabled: bool,
99 pub pipeline_batch_size: usize,
101}
102
103impl Default for RedisConfig {
104 fn default() -> Self {
105 Self {
106 urls: vec!["redis://localhost:6379".to_string()],
107 key_prefix: "state:".to_string(),
108 default_ttl: None,
109 min_connections: 5,
110 max_connections: 50,
111 connect_timeout: Duration::from_secs(5),
112 read_timeout: Duration::from_secs(3),
113 write_timeout: Duration::from_secs(3),
114 max_retries: 3,
115 retry_base_delay: Duration::from_millis(100),
116 retry_max_delay: Duration::from_secs(5),
117 cluster_mode: false,
118 tls_enabled: false,
119 pipeline_batch_size: 100,
120 }
121 }
122}
123
124impl RedisConfig {
125 pub fn builder() -> RedisConfigBuilder {
127 RedisConfigBuilder::default()
128 }
129
130 pub fn primary_url(&self) -> &str {
132 self.urls.first().map(|s| s.as_str()).unwrap_or("redis://localhost:6379")
133 }
134}
135
136#[derive(Debug, Default)]
138pub struct RedisConfigBuilder {
139 config: RedisConfig,
140}
141
142impl RedisConfigBuilder {
143 pub fn url(mut self, url: impl Into<String>) -> Self {
145 self.config.urls = vec![url.into()];
146 self
147 }
148
149 pub fn urls(mut self, urls: Vec<String>) -> Self {
151 self.config.cluster_mode = urls.len() > 1;
152 self.config.urls = urls;
153 self
154 }
155
156 pub fn key_prefix(mut self, prefix: impl Into<String>) -> Self {
158 self.config.key_prefix = prefix.into();
159 self
160 }
161
162 pub fn default_ttl(mut self, ttl: Duration) -> Self {
164 self.config.default_ttl = Some(ttl);
165 self
166 }
167
168 pub fn pool_size(mut self, min: u32, max: u32) -> Self {
170 self.config.min_connections = min;
171 self.config.max_connections = max;
172 self
173 }
174
175 pub fn timeouts(mut self, connect: Duration, read: Duration, write: Duration) -> Self {
177 self.config.connect_timeout = connect;
178 self.config.read_timeout = read;
179 self.config.write_timeout = write;
180 self
181 }
182
183 pub fn retries(mut self, max: u32, base_delay: Duration, max_delay: Duration) -> Self {
185 self.config.max_retries = max;
186 self.config.retry_base_delay = base_delay;
187 self.config.retry_max_delay = max_delay;
188 self
189 }
190
191 pub fn cluster_mode(mut self, enabled: bool) -> Self {
193 self.config.cluster_mode = enabled;
194 self
195 }
196
197 pub fn tls(mut self, enabled: bool) -> Self {
199 self.config.tls_enabled = enabled;
200 self
201 }
202
203 pub fn pipeline_batch_size(mut self, size: usize) -> Self {
205 self.config.pipeline_batch_size = size;
206 self
207 }
208
209 pub fn build(self) -> StateResult<RedisConfig> {
211 if self.config.urls.is_empty() {
212 return Err(StateError::StorageError {
213 backend_type: "redis".to_string(),
214 details: "No Redis URLs configured".to_string(),
215 });
216 }
217
218 if self.config.max_connections < self.config.min_connections {
219 return Err(StateError::StorageError {
220 backend_type: "redis".to_string(),
221 details: "max_connections must be >= min_connections".to_string(),
222 });
223 }
224
225 Ok(self.config)
226 }
227}
228
229#[derive(Debug, Clone, Default, Serialize, Deserialize)]
231pub struct RedisStats {
232 pub get_count: u64,
234 pub put_count: u64,
236 pub delete_count: u64,
238 pub hit_count: u64,
240 pub miss_count: u64,
242 pub pipeline_count: u64,
244 pub retry_count: u64,
246 pub error_count: u64,
248 pub bytes_read: u64,
250 pub bytes_written: u64,
252 pub avg_latency_us: u64,
254 pub active_connections: u32,
256}
257
258impl RedisStats {
259 pub fn hit_rate(&self) -> f64 {
261 let total = self.hit_count + self.miss_count;
262 if total == 0 {
263 0.0
264 } else {
265 self.hit_count as f64 / total as f64
266 }
267 }
268
269 pub fn error_rate(&self) -> f64 {
271 let total = self.get_count + self.put_count + self.delete_count;
272 if total == 0 {
273 0.0
274 } else {
275 self.error_count as f64 / total as f64
276 }
277 }
278
279 pub fn total_operations(&self) -> u64 {
281 self.get_count + self.put_count + self.delete_count
282 }
283}
284
285pub struct RedisStateBackend {
287 connection: Arc<ConnectionManager>,
289 config: RedisConfig,
291 stats: Arc<RwLock<RedisStats>>,
293 scripts: RedisScripts,
295}
296
297struct RedisScripts {
299 get_delete: Script,
301 incr_with_ttl: Script,
303 set_nx_with_ttl: Script,
305}
306
307impl RedisScripts {
308 fn new() -> Self {
309 Self {
310 get_delete: Script::new(
312 r"
313 local value = redis.call('GET', KEYS[1])
314 if value then
315 redis.call('DEL', KEYS[1])
316 end
317 return value
318 "
319 ),
320 incr_with_ttl: Script::new(
322 r"
323 local value = redis.call('INCR', KEYS[1])
324 redis.call('EXPIRE', KEYS[1], ARGV[1])
325 return value
326 "
327 ),
328 set_nx_with_ttl: Script::new(
330 r"
331 local exists = redis.call('EXISTS', KEYS[1])
332 if exists == 0 then
333 redis.call('SET', KEYS[1], ARGV[1])
334 redis.call('EXPIRE', KEYS[1], ARGV[2])
335 return 1
336 end
337 return 0
338 "
339 ),
340 }
341 }
342}
343
344impl RedisStateBackend {
345 pub async fn new(config: RedisConfig) -> StateResult<Self> {
359 let client = Self::create_client(&config)?;
360 let connection = Self::create_connection_manager(client, &config).await?;
361
362 Ok(Self {
363 connection: Arc::new(connection),
364 config,
365 stats: Arc::new(RwLock::new(RedisStats::default())),
366 scripts: RedisScripts::new(),
367 })
368 }
369
370 fn create_client(config: &RedisConfig) -> StateResult<Client> {
372 let url = config.primary_url();
373 Client::open(url).map_err(|e| StateError::StorageError {
374 backend_type: "redis".to_string(),
375 details: format!("Failed to create client: {}", e),
376 })
377 }
378
379 async fn create_connection_manager(
381 client: Client,
382 config: &RedisConfig,
383 ) -> StateResult<ConnectionManager> {
384 let mut retries = 0;
385 let mut delay = config.retry_base_delay;
386
387 loop {
388 match ConnectionManager::new(client.clone()).await {
389 Ok(conn) => {
390 debug!("Redis connection established");
391 return Ok(conn);
392 }
393 Err(e) => {
394 retries += 1;
395 if retries >= config.max_retries {
396 error!("Failed to connect to Redis after {} retries: {}", retries, e);
397 return Err(StateError::StorageError {
398 backend_type: "redis".to_string(),
399 details: format!("Connection failed after {} retries: {}", retries, e),
400 });
401 }
402
403 warn!("Redis connection attempt {} failed: {}, retrying in {:?}",
404 retries, e, delay);
405 tokio::time::sleep(delay).await;
406 delay = std::cmp::min(delay * 2, config.retry_max_delay);
407 }
408 }
409 }
410 }
411
412 fn build_key(&self, key: &[u8]) -> Vec<u8> {
414 let mut full_key = self.config.key_prefix.as_bytes().to_vec();
415 full_key.extend_from_slice(key);
416 full_key
417 }
418
419 fn strip_prefix(&self, key: &[u8]) -> Vec<u8> {
421 let prefix = self.config.key_prefix.as_bytes();
422 if key.starts_with(prefix) {
423 key[prefix.len()..].to_vec()
424 } else {
425 key.to_vec()
426 }
427 }
428
429 async fn with_retry<F, Fut, T>(&self, mut operation: F) -> StateResult<T>
431 where
432 F: FnMut() -> Fut,
433 Fut: std::future::Future<Output = RedisResult<T>>,
434 {
435 let mut retries = 0;
436 let mut delay = self.config.retry_base_delay;
437
438 loop {
439 match operation().await {
440 Ok(result) => return Ok(result),
441 Err(e) => {
442 retries += 1;
443
444 let mut stats = self.stats.write().await;
446 stats.error_count += 1;
447 stats.retry_count += 1;
448 drop(stats);
449
450 if retries >= self.config.max_retries {
451 error!("Operation failed after {} retries: {}", retries, e);
452 return Err(StateError::StorageError {
453 backend_type: "redis".to_string(),
454 details: format!("Operation failed after {} retries: {}", retries, e),
455 });
456 }
457
458 warn!("Operation attempt {} failed: {}, retrying in {:?}",
459 retries, e, delay);
460 tokio::time::sleep(delay).await;
461 delay = std::cmp::min(delay * 2, self.config.retry_max_delay);
462 }
463 }
464 }
465 }
466
467 pub async fn stats(&self) -> RedisStats {
469 self.stats.read().await.clone()
470 }
471
472 pub async fn health_check(&self) -> StateResult<bool> {
474 let mut conn = self.connection.as_ref().clone();
475 let result: RedisResult<String> = redis::cmd("PING").query_async(&mut conn).await;
476
477 match result {
478 Ok(response) => Ok(response == "PONG"),
479 Err(e) => {
480 error!("Health check failed: {}", e);
481 Err(StateError::StorageError {
482 backend_type: "redis".to_string(),
483 details: format!("Health check failed: {}", e),
484 })
485 }
486 }
487 }
488
489 pub async fn server_info(&self) -> StateResult<String> {
491 let mut conn = self.connection.as_ref().clone();
492 redis::cmd("INFO")
493 .query_async(&mut conn)
494 .await
495 .map_err(|e| StateError::StorageError {
496 backend_type: "redis".to_string(),
497 details: format!("Failed to get server info: {}", e),
498 })
499 }
500
501 pub async fn put_with_ttl(&self, key: &[u8], value: &[u8], ttl: Duration) -> StateResult<()> {
503 let full_key = self.build_key(key);
504 let ttl_secs = ttl.as_secs();
505
506 let conn = self.connection.as_ref().clone();
507 let full_key_clone = full_key.clone();
508 let value_clone = value.to_vec();
509
510 let result = self.with_retry(move || {
511 let mut conn = conn.clone();
512 let full_key = full_key_clone.clone();
513 let value = value_clone.clone();
514 async move {
515 conn.set_ex(&full_key, &value, ttl_secs).await
516 }
517 }).await;
518
519 if result.is_ok() {
520 let mut stats = self.stats.write().await;
521 stats.put_count += 1;
522 stats.bytes_written += value.len() as u64;
523 }
524
525 result
526 }
527
528 pub async fn batch_get(&self, keys: &[&[u8]]) -> StateResult<Vec<Option<Vec<u8>>>> {
530 if keys.is_empty() {
531 return Ok(Vec::new());
532 }
533
534 let full_keys: Vec<Vec<u8>> = keys.iter().map(|k| self.build_key(k)).collect();
535 let conn = self.connection.as_ref().clone();
536
537 let full_keys_clone = full_keys.clone();
538 let result: Vec<Option<Vec<u8>>> = self.with_retry(move || {
539 let mut conn = conn.clone();
540 let full_keys = full_keys_clone.clone();
541 async move {
542 redis::cmd("MGET")
543 .arg(&full_keys)
544 .query_async(&mut conn)
545 .await
546 }
547 }).await?;
548
549 let mut stats = self.stats.write().await;
551 stats.get_count += keys.len() as u64;
552 for value in &result {
553 if let Some(v) = value {
554 stats.hit_count += 1;
555 stats.bytes_read += v.len() as u64;
556 } else {
557 stats.miss_count += 1;
558 }
559 }
560
561 Ok(result)
562 }
563
564 pub async fn batch_put(&self, pairs: &[(&[u8], &[u8])]) -> StateResult<()> {
566 if pairs.is_empty() {
567 return Ok(());
568 }
569
570 for chunk in pairs.chunks(self.config.pipeline_batch_size) {
572 let mut conn = self.connection.as_ref().clone();
573
574 let commands: Vec<(Vec<u8>, Vec<u8>, Option<u64>)> = chunk
576 .iter()
577 .map(|(key, value)| {
578 let full_key = self.build_key(key);
579 let ttl = self.config.default_ttl.map(|d| d.as_secs());
580 (full_key, value.to_vec(), ttl)
581 })
582 .collect();
583
584 self.with_retry(move || {
585 let mut conn = conn.clone();
586 let commands = commands.clone();
587 async move {
588 let mut pipe = redis::pipe();
589 for (full_key, value, ttl) in &commands {
590 if let Some(ttl_secs) = ttl {
591 pipe.set_ex(full_key, value, *ttl_secs);
592 } else {
593 pipe.set(full_key, value);
594 }
595 }
596 pipe.query_async(&mut conn).await
597 }
598 }).await?;
599
600 let mut stats = self.stats.write().await;
602 stats.put_count += chunk.len() as u64;
603 stats.pipeline_count += 1;
604 stats.bytes_written += chunk.iter().map(|(_, v)| v.len() as u64).sum::<u64>();
605 }
606
607 Ok(())
608 }
609
610 pub async fn batch_delete(&self, keys: &[&[u8]]) -> StateResult<usize> {
612 if keys.is_empty() {
613 return Ok(0);
614 }
615
616 let full_keys: Vec<Vec<u8>> = keys.iter().map(|k| self.build_key(k)).collect();
617 let conn = self.connection.as_ref().clone();
618 let full_keys_clone = full_keys.clone();
619
620 let deleted: usize = self.with_retry(move || {
621 let mut conn = conn.clone();
622 let full_keys = full_keys_clone.clone();
623 async move {
624 redis::cmd("DEL")
625 .arg(&full_keys)
626 .query_async(&mut conn)
627 .await
628 }
629 }).await?;
630
631 let mut stats = self.stats.write().await;
633 stats.delete_count += keys.len() as u64;
634
635 Ok(deleted)
636 }
637
638 async fn scan_keys_internal(&self, pattern: Vec<u8>, count: usize) -> StateResult<Vec<Vec<u8>>> {
640 let mut conn = self.connection.as_ref().clone();
641 let mut cursor = 0u64;
642 let mut all_keys = Vec::new();
643
644 loop {
645 let (new_cursor, keys): (u64, Vec<Vec<u8>>) = redis::cmd("SCAN")
646 .arg(cursor)
647 .arg("MATCH")
648 .arg(&pattern)
649 .arg("COUNT")
650 .arg(count)
651 .query_async(&mut conn)
652 .await
653 .map_err(|e| StateError::StorageError {
654 backend_type: "redis".to_string(),
655 details: format!("SCAN failed: {}", e),
656 })?;
657
658 all_keys.extend(keys);
659 cursor = new_cursor;
660
661 if cursor == 0 {
662 break;
663 }
664 }
665
666 Ok(all_keys)
667 }
668
669 pub async fn get_and_delete(&self, key: &[u8]) -> StateResult<Option<Vec<u8>>> {
671 let full_key = self.build_key(key);
672 let mut conn = self.connection.as_ref().clone();
673
674 let result: Option<Vec<u8>> = self.scripts
675 .get_delete
676 .key(&full_key)
677 .invoke_async(&mut conn)
678 .await
679 .map_err(|e| StateError::StorageError {
680 backend_type: "redis".to_string(),
681 details: format!("GET_DELETE script failed: {}", e),
682 })?;
683
684 let mut stats = self.stats.write().await;
686 stats.get_count += 1;
687 stats.delete_count += 1;
688
689 if result.is_some() {
690 stats.hit_count += 1;
691 } else {
692 stats.miss_count += 1;
693 }
694
695 Ok(result)
696 }
697
698 pub async fn set_if_not_exists(&self, key: &[u8], value: &[u8], ttl: Duration) -> StateResult<bool> {
700 let full_key = self.build_key(key);
701 let ttl_secs = ttl.as_secs();
702 let mut conn = self.connection.as_ref().clone();
703
704 let result: i32 = self.scripts
705 .set_nx_with_ttl
706 .key(&full_key)
707 .arg(value)
708 .arg(ttl_secs)
709 .invoke_async(&mut conn)
710 .await
711 .map_err(|e| StateError::StorageError {
712 backend_type: "redis".to_string(),
713 details: format!("SET_NX_WITH_TTL script failed: {}", e),
714 })?;
715
716 if result == 1 {
717 let mut stats = self.stats.write().await;
718 stats.put_count += 1;
719 stats.bytes_written += value.len() as u64;
720 }
721
722 Ok(result == 1)
723 }
724
725 pub async fn ttl(&self, key: &[u8]) -> StateResult<Option<Duration>> {
727 let full_key = self.build_key(key);
728 let mut conn = self.connection.as_ref().clone();
729
730 let ttl_secs: i64 = redis::cmd("TTL")
731 .arg(&full_key)
732 .query_async(&mut conn)
733 .await
734 .map_err(|e| StateError::StorageError {
735 backend_type: "redis".to_string(),
736 details: format!("TTL query failed: {}", e),
737 })?;
738
739 match ttl_secs {
740 -2 => Ok(None), -1 => Ok(Some(Duration::MAX)), secs if secs >= 0 => Ok(Some(Duration::from_secs(secs as u64))),
743 _ => Ok(None),
744 }
745 }
746
747 pub async fn checkpoint(&self) -> StateResult<Vec<(Vec<u8>, Vec<u8>)>> {
749 let pattern = format!("{}*", self.config.key_prefix).into_bytes();
750 let keys = self.scan_keys_internal(pattern, 1000).await?;
751
752 if keys.is_empty() {
753 return Ok(Vec::new());
754 }
755
756 let key_refs: Vec<&[u8]> = keys.iter().map(|k| k.as_slice()).collect();
758 let values = self.batch_get(&key_refs).await?;
759
760 let checkpoint: Vec<(Vec<u8>, Vec<u8>)> = keys
762 .into_iter()
763 .zip(values)
764 .filter_map(|(k, v)| v.map(|val| (self.strip_prefix(&k), val)))
765 .collect();
766
767 Ok(checkpoint)
768 }
769
770 pub async fn restore(&self, checkpoint: Vec<(Vec<u8>, Vec<u8>)>) -> StateResult<()> {
772 if checkpoint.is_empty() {
773 return Ok(());
774 }
775
776 let pairs: Vec<(&[u8], &[u8])> = checkpoint
777 .iter()
778 .map(|(k, v)| (k.as_slice(), v.as_slice()))
779 .collect();
780
781 self.batch_put(&pairs).await
782 }
783
784 pub async fn memory_usage(&self) -> StateResult<u64> {
786 let mut conn = self.connection.as_ref().clone();
787
788 let info: String = redis::cmd("INFO")
789 .arg("memory")
790 .query_async(&mut conn)
791 .await
792 .map_err(|e| StateError::StorageError {
793 backend_type: "redis".to_string(),
794 details: format!("Failed to get memory info: {}", e),
795 })?;
796
797 for line in info.lines() {
799 if line.starts_with("used_memory:") {
800 if let Some(bytes_str) = line.split(':').nth(1) {
801 if let Ok(bytes) = bytes_str.trim().parse::<u64>() {
802 return Ok(bytes);
803 }
804 }
805 }
806 }
807
808 Ok(0)
809 }
810}
811
812#[async_trait]
813impl StateBackend for RedisStateBackend {
814 async fn get(&self, key: &[u8]) -> StateResult<Option<Vec<u8>>> {
815 let start = std::time::Instant::now();
816 let full_key = self.build_key(key);
817 trace!("Getting key: {:?}", full_key);
818
819 let conn = self.connection.as_ref().clone();
820 let full_key_clone = full_key.clone();
821
822 let result: Option<Vec<u8>> = self.with_retry(move || {
823 let mut conn = conn.clone();
824 let full_key = full_key_clone.clone();
825 async move {
826 conn.get(&full_key).await
827 }
828 }).await?;
829
830 let mut stats = self.stats.write().await;
832 stats.get_count += 1;
833
834 if let Some(ref value) = result {
835 stats.hit_count += 1;
836 stats.bytes_read += value.len() as u64;
837 } else {
838 stats.miss_count += 1;
839 }
840
841 let latency_us = start.elapsed().as_micros() as u64;
843 stats.avg_latency_us = if stats.total_operations() == 1 {
844 latency_us
845 } else {
846 (stats.avg_latency_us * 9 + latency_us) / 10
847 };
848
849 Ok(result)
850 }
851
852 async fn put(&self, key: &[u8], value: &[u8]) -> StateResult<()> {
853 let full_key = self.build_key(key);
854 trace!("Putting key: {:?}, value size: {} bytes", full_key, value.len());
855
856 let conn = self.connection.as_ref().clone();
857 let full_key_clone = full_key.clone();
858 let value_clone = value.to_vec();
859
860 if let Some(ttl) = self.config.default_ttl {
861 let ttl_secs = ttl.as_secs();
862 self.with_retry(move || {
863 let mut conn = conn.clone();
864 let full_key = full_key_clone.clone();
865 let value = value_clone.clone();
866 async move {
867 conn.set_ex(&full_key, &value, ttl_secs).await
868 }
869 }).await?;
870 } else {
871 self.with_retry(move || {
872 let mut conn = conn.clone();
873 let full_key = full_key_clone.clone();
874 let value = value_clone.clone();
875 async move {
876 conn.set(&full_key, &value).await
877 }
878 }).await?;
879 }
880
881 let mut stats = self.stats.write().await;
883 stats.put_count += 1;
884 stats.bytes_written += value.len() as u64;
885
886 Ok(())
887 }
888
889 async fn delete(&self, key: &[u8]) -> StateResult<()> {
890 let full_key = self.build_key(key);
891 trace!("Deleting key: {:?}", full_key);
892
893 let conn = self.connection.as_ref().clone();
894 let full_key_clone = full_key.clone();
895
896 self.with_retry(move || {
897 let mut conn = conn.clone();
898 let full_key = full_key_clone.clone();
899 async move {
900 conn.del(&full_key).await
901 }
902 }).await?;
903
904 let mut stats = self.stats.write().await;
906 stats.delete_count += 1;
907
908 Ok(())
909 }
910
911 async fn list_keys(&self, prefix: &[u8]) -> StateResult<Vec<Vec<u8>>> {
912 trace!("Listing keys with prefix: {:?}", prefix);
913
914 let pattern = if prefix.is_empty() {
915 format!("{}*", self.config.key_prefix).into_bytes()
916 } else {
917 let mut p = self.config.key_prefix.as_bytes().to_vec();
918 p.extend_from_slice(prefix);
919 p.push(b'*');
920 p
921 };
922
923 let keys = self.scan_keys_internal(pattern, 1000).await?;
924
925 let stripped_keys = keys
927 .into_iter()
928 .map(|k| self.strip_prefix(&k))
929 .collect();
930
931 Ok(stripped_keys)
932 }
933
934 async fn clear(&self) -> StateResult<()> {
935 debug!("Clearing all state");
936
937 let pattern = format!("{}*", self.config.key_prefix).into_bytes();
938 let keys = self.scan_keys_internal(pattern, 1000).await?;
939
940 if keys.is_empty() {
941 return Ok(());
942 }
943
944 let key_refs: Vec<&[u8]> = keys.iter().map(|k| k.as_slice()).collect();
946 for chunk in key_refs.chunks(self.config.pipeline_batch_size) {
947 let chunk_vec: Vec<Vec<u8>> = chunk.iter().map(|k| k.to_vec()).collect();
948
949 let mut retries = 0;
951 let mut delay = self.config.retry_base_delay;
952
953 loop {
954 let mut conn = self.connection.as_ref().clone();
955
956 let result: RedisResult<()> = redis::cmd("DEL")
957 .arg(&chunk_vec)
958 .query_async(&mut conn)
959 .await;
960
961 match result {
962 Ok(_) => break,
963 Err(e) => {
964 retries += 1;
965
966 if retries >= self.config.max_retries {
967 return Err(StateError::StorageError {
968 backend_type: "redis".to_string(),
969 details: format!("Delete batch failed after {} retries: {}", retries, e),
970 });
971 }
972
973 warn!("Delete batch attempt {} failed: {}, retrying in {:?}", retries, e, delay);
974 tokio::time::sleep(delay).await;
975 delay = std::cmp::min(delay * 2, self.config.retry_max_delay);
976 }
977 }
978 }
979 }
980
981 Ok(())
982 }
983
984 async fn count(&self) -> StateResult<usize> {
985 let pattern = format!("{}*", self.config.key_prefix).into_bytes();
986 let keys = self.scan_keys_internal(pattern, 1000).await?;
987 Ok(keys.len())
988 }
989
990 async fn contains(&self, key: &[u8]) -> StateResult<bool> {
991 let full_key = self.build_key(key);
992 let conn = self.connection.as_ref().clone();
993 let full_key_clone = full_key.clone();
994
995 let exists: bool = self.with_retry(move || {
996 let mut conn = conn.clone();
997 let full_key = full_key_clone.clone();
998 async move {
999 conn.exists(&full_key).await
1000 }
1001 }).await?;
1002
1003 Ok(exists)
1004 }
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009 use super::*;
1010 use crate::state::backend::tests::*;
1011
1012 async fn create_test_backend() -> Option<RedisStateBackend> {
1014 let config = RedisConfig::builder()
1015 .url("redis://localhost:6379")
1016 .key_prefix("test:")
1017 .build()
1018 .ok()?;
1019
1020 match RedisStateBackend::new(config).await {
1021 Ok(backend) => {
1022 let _ = backend.clear().await;
1024 Some(backend)
1025 }
1026 Err(_) => None,
1027 }
1028 }
1029
1030 #[tokio::test]
1031 async fn test_redis_backend_basic() {
1032 if let Some(backend) = create_test_backend().await {
1033 test_backend_basic_ops(backend).await;
1034 } else {
1035 eprintln!("Skipping test: Redis not available");
1036 }
1037 }
1038
1039 #[tokio::test]
1040 async fn test_redis_backend_list_keys() {
1041 if let Some(backend) = create_test_backend().await {
1042 test_backend_list_keys(backend).await;
1043 } else {
1044 eprintln!("Skipping test: Redis not available");
1045 }
1046 }
1047
1048 #[tokio::test]
1049 async fn test_redis_backend_clear() {
1050 if let Some(backend) = create_test_backend().await {
1051 test_backend_clear(backend).await;
1052 } else {
1053 eprintln!("Skipping test: Redis not available");
1054 }
1055 }
1056
1057 #[tokio::test]
1058 async fn test_redis_backend_contains() {
1059 if let Some(backend) = create_test_backend().await {
1060 test_backend_contains(backend).await;
1061 } else {
1062 eprintln!("Skipping test: Redis not available");
1063 }
1064 }
1065
1066 #[tokio::test]
1067 async fn test_redis_ttl() {
1068 if let Some(backend) = create_test_backend().await {
1069 backend
1070 .put_with_ttl(b"ttl_key", b"value", Duration::from_secs(2))
1071 .await
1072 .unwrap();
1073
1074 assert!(backend.get(b"ttl_key").await.unwrap().is_some());
1075
1076 let ttl = backend.ttl(b"ttl_key").await.unwrap();
1078 assert!(ttl.is_some());
1079 assert!(ttl.unwrap().as_secs() <= 2);
1080
1081 tokio::time::sleep(Duration::from_secs(3)).await;
1083 assert!(backend.get(b"ttl_key").await.unwrap().is_none());
1084 } else {
1085 eprintln!("Skipping test: Redis not available");
1086 }
1087 }
1088
1089 #[tokio::test]
1090 async fn test_redis_batch_operations() {
1091 if let Some(backend) = create_test_backend().await {
1092 let pairs = vec![
1094 (&b"batch1"[..], &b"value1"[..]),
1095 (&b"batch2"[..], &b"value2"[..]),
1096 (&b"batch3"[..], &b"value3"[..]),
1097 ];
1098 backend.batch_put(&pairs).await.unwrap();
1099
1100 let keys = vec![b"batch1", b"batch2", b"batch3"];
1102 let values = backend.batch_get(&keys).await.unwrap();
1103 assert_eq!(values.len(), 3);
1104 assert_eq!(values[0], Some(b"value1".to_vec()));
1105 assert_eq!(values[1], Some(b"value2".to_vec()));
1106 assert_eq!(values[2], Some(b"value3".to_vec()));
1107
1108 let deleted = backend.batch_delete(&keys).await.unwrap();
1110 assert_eq!(deleted, 3);
1111 } else {
1112 eprintln!("Skipping test: Redis not available");
1113 }
1114 }
1115
1116 #[tokio::test]
1117 async fn test_redis_atomic_operations() {
1118 if let Some(backend) = create_test_backend().await {
1119 backend.put(b"atomic_key", b"value").await.unwrap();
1120
1121 let value = backend.get_and_delete(b"atomic_key").await.unwrap();
1123 assert_eq!(value, Some(b"value".to_vec()));
1124
1125 assert!(backend.get(b"atomic_key").await.unwrap().is_none());
1127
1128 let set1 = backend
1130 .set_if_not_exists(b"nx_key", b"value1", Duration::from_secs(60))
1131 .await
1132 .unwrap();
1133 assert!(set1);
1134
1135 let set2 = backend
1136 .set_if_not_exists(b"nx_key", b"value2", Duration::from_secs(60))
1137 .await
1138 .unwrap();
1139 assert!(!set2);
1140
1141 assert_eq!(
1143 backend.get(b"nx_key").await.unwrap(),
1144 Some(b"value1".to_vec())
1145 );
1146 } else {
1147 eprintln!("Skipping test: Redis not available");
1148 }
1149 }
1150
1151 #[tokio::test]
1152 async fn test_redis_checkpoint_restore() {
1153 if let Some(backend) = create_test_backend().await {
1154 backend.put(b"key1", b"value1").await.unwrap();
1155 backend.put(b"key2", b"value2").await.unwrap();
1156
1157 let checkpoint = backend.checkpoint().await.unwrap();
1159 assert_eq!(checkpoint.len(), 2);
1160
1161 backend.clear().await.unwrap();
1163 assert_eq!(backend.count().await.unwrap(), 0);
1164
1165 backend.restore(checkpoint).await.unwrap();
1166 assert_eq!(backend.count().await.unwrap(), 2);
1167
1168 assert_eq!(
1169 backend.get(b"key1").await.unwrap(),
1170 Some(b"value1".to_vec())
1171 );
1172 } else {
1173 eprintln!("Skipping test: Redis not available");
1174 }
1175 }
1176
1177 #[tokio::test]
1178 async fn test_redis_stats() {
1179 if let Some(backend) = create_test_backend().await {
1180 backend.put(b"key1", b"value1").await.unwrap();
1181 backend.put(b"key2", b"value2").await.unwrap();
1182
1183 backend.get(b"key1").await.unwrap(); backend.get(b"key1").await.unwrap(); backend.get(b"nonexistent").await.unwrap(); backend.delete(b"key1").await.unwrap();
1188
1189 let stats = backend.stats().await;
1190 assert_eq!(stats.put_count, 2);
1191 assert_eq!(stats.get_count, 3);
1192 assert_eq!(stats.delete_count, 1);
1193 assert_eq!(stats.hit_count, 2);
1194 assert_eq!(stats.miss_count, 1);
1195 assert!(stats.hit_rate() > 0.0);
1196 } else {
1197 eprintln!("Skipping test: Redis not available");
1198 }
1199 }
1200
1201 #[tokio::test]
1202 async fn test_redis_health_check() {
1203 if let Some(backend) = create_test_backend().await {
1204 let healthy = backend.health_check().await.unwrap();
1205 assert!(healthy);
1206 } else {
1207 eprintln!("Skipping test: Redis not available");
1208 }
1209 }
1210
1211 #[tokio::test]
1212 async fn test_config_builder() {
1213 let config = RedisConfig::builder()
1214 .url("redis://localhost:6379")
1215 .key_prefix("myapp:")
1216 .default_ttl(Duration::from_secs(3600))
1217 .pool_size(10, 50)
1218 .cluster_mode(false)
1219 .build()
1220 .unwrap();
1221
1222 assert_eq!(config.urls.len(), 1);
1223 assert_eq!(config.key_prefix, "myapp:");
1224 assert_eq!(config.default_ttl, Some(Duration::from_secs(3600)));
1225 assert_eq!(config.min_connections, 10);
1226 assert_eq!(config.max_connections, 50);
1227 assert!(!config.cluster_mode);
1228 }
1229}