mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
#![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")
    }
}