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