mecha10_cli/services/
redis.rs

1#![allow(dead_code)]
2
3//! Redis service for managing Redis connections and operations
4//!
5//! This service provides a centralized, reusable interface for all Redis operations
6//! across the CLI. It handles connection management, health checks, and common
7//! Redis stream operations.
8
9use anyhow::{Context, Result};
10use redis::aio::MultiplexedConnection as RedisConnection;
11use std::time::Duration;
12
13/// Redis service for connection management and operations
14///
15/// # Examples
16///
17/// ```rust,ignore
18/// use mecha10_cli::services::RedisService;
19///
20/// // Create service with default URL
21/// let redis = RedisService::default();
22///
23/// // Create service with custom URL
24/// let redis = RedisService::new("redis://localhost:6380")?;
25///
26/// // Get a connection
27/// let mut conn = redis.get_connection().await?;
28/// ```
29#[derive(Debug)]
30pub struct RedisService {
31    client: redis::Client,
32    url: String,
33}
34
35impl RedisService {
36    /// Create a new Redis service with a custom URL
37    ///
38    /// # Arguments
39    ///
40    /// * `url` - Redis connection URL (e.g., "redis://localhost:6379")
41    ///
42    /// # Errors
43    ///
44    /// Returns an error if the Redis client cannot be created with the provided URL.
45    pub fn new(url: &str) -> Result<Self> {
46        let client =
47            redis::Client::open(url).with_context(|| format!("Failed to create Redis client for URL: {}", url))?;
48
49        Ok(Self {
50            client,
51            url: url.to_string(),
52        })
53    }
54
55    /// Create a new Redis service with default URL
56    ///
57    /// Reads URL from environment variables in order of precedence:
58    /// 1. `MECHA10_REDIS_URL`
59    /// 2. `REDIS_URL`
60    /// 3. Default: "redis://localhost:6379"
61    ///
62    /// # Errors
63    ///
64    /// Returns an error if the Redis client cannot be created.
65    pub fn from_env() -> Result<Self> {
66        let url = std::env::var("MECHA10_REDIS_URL")
67            .or_else(|_| std::env::var("REDIS_URL"))
68            .unwrap_or_else(|_| "redis://localhost:6379".to_string());
69
70        Self::new(&url)
71    }
72
73    /// Get the Redis URL being used
74    pub fn url(&self) -> &str {
75        &self.url
76    }
77
78    /// Get an async Redis connection
79    ///
80    /// This creates a new connection each time. For long-running operations,
81    /// consider caching the connection in your code.
82    ///
83    /// # Errors
84    ///
85    /// Returns an error if the connection cannot be established.
86    pub async fn get_connection(&self) -> Result<RedisConnection> {
87        self.client
88            .get_multiplexed_async_connection()
89            .await
90            .context("Failed to establish async Redis connection")
91    }
92
93    /// Get a multiplexed async Redis connection
94    ///
95    /// Multiplexed connections allow multiple concurrent operations on the same connection.
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if the connection cannot be established.
100    pub async fn get_multiplexed_connection(&self) -> Result<redis::aio::MultiplexedConnection> {
101        self.client
102            .get_multiplexed_async_connection()
103            .await
104            .context("Failed to establish multiplexed Redis connection")
105    }
106
107    /// Check if Redis is healthy and reachable
108    ///
109    /// Attempts to connect and execute a PING command within the specified timeout.
110    ///
111    /// # Arguments
112    ///
113    /// * `timeout` - Maximum time to wait for health check
114    ///
115    /// # Returns
116    ///
117    /// Returns `true` if Redis is reachable and responds to PING, `false` otherwise.
118    pub async fn check_health(&self, timeout: Duration) -> bool {
119        match tokio::time::timeout(timeout, async {
120            let mut conn = self.get_multiplexed_connection().await?;
121            let _: String = redis::cmd("PING").query_async(&mut conn).await?;
122            Ok::<(), anyhow::Error>(())
123        })
124        .await
125        {
126            Ok(Ok(())) => true,
127            Ok(Err(_)) => false,
128            Err(_) => false, // Timeout
129        }
130    }
131
132    /// Check health with default timeout (2 seconds)
133    pub async fn is_healthy(&self) -> bool {
134        self.check_health(Duration::from_secs(2)).await
135    }
136}
137
138impl Default for RedisService {
139    /// Create a Redis service with environment-based configuration
140    ///
141    /// # Panics
142    ///
143    /// Panics if the Redis client cannot be created. For error handling,
144    /// use `RedisService::from_env()` instead.
145    fn default() -> Self {
146        Self::from_env().expect("Failed to create default Redis service")
147    }
148}