Skip to main content

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