darkwing/cache/
connection.rs

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
//! Redis cache connection management module.
//!
//! This module provides functionality for establishing and managing connections
//! to a Redis cache using connection pooling. It handles connection
//! initialization and pooling configuration.

use std::time::Duration;

use anyhow::{Context, Ok};
use r2d2::Pool;
use tracing::info;

/// Type alias for the Redis connection pool.
pub type ConnectionPool = Pool<redis::Client>;

/// Represents a Redis cache connection manager.
///
/// The `Cache` struct maintains a connection pool to Redis and provides methods
/// for establishing connections with configurable parameters.
#[derive(Debug, Clone)]
pub struct Cache {
  /// The connection pool for Redis connections.
  pub pool: ConnectionPool,
}

impl Cache {
  /// Establishes a new connection to Redis with the specified configuration.
  ///
  /// # Arguments
  ///
  /// * `connection_string` - The Redis connection URL (e.g.,
  ///   "redis://:password@127.0.0.1:6379")
  /// * `connection_pool_size` - Maximum number of connections in the pool
  /// * `connection_timeout` - Maximum time to wait for a connection
  ///
  /// # Returns
  ///
  /// Returns a `Result` containing the `Cache` instance if successful, or an
  /// error if the connection could not be established.
  ///
  /// # Examples
  ///
  /// ```rust,no_run
  /// use std::time::Duration;
  /// use darkwing::cache::Cache;
  ///
  /// async fn example() -> anyhow::Result<()> {
  ///   let cache = Cache::connect(
  ///     "redis://127.0.0.1:6379",
  ///     5,
  ///     Duration::from_secs(10)
  ///   )?;
  ///   Ok(())
  /// }
  /// ```
  pub fn connect(
    connection_string: &str,
    connection_pool_size: u32,
    connection_timeout: Duration,
  ) -> anyhow::Result<Self> {
    info!("Connecting to Redis cache...");

    let client = redis::Client::open(connection_string)
      .context("error creating Redis client")?;

    let pool = Pool::builder()
      .max_size(connection_pool_size)
      .connection_timeout(connection_timeout)
      .build(client)
      .context("error while initializing the Redis connection pool")?;

    info!("Connected to Redis cache");

    Ok(Self { pool })
  }
}

#[cfg(test)]
mod tests {
  use super::*;
  use redis::Commands;
  use testcontainers::runners::AsyncRunner;
  use testcontainers_modules::redis::Redis;

  #[tokio::test]
  async fn test_database_connection() -> anyhow::Result<()> {
    let redis_container = Redis::default().start().await?;
    let host = redis_container.get_host().await?.to_string();
    let port = redis_container.get_host_port_ipv4(6379).await?;

    let connection_string = format!("redis://{host}:{port}");

    let db = Cache::connect(&connection_string, 5, Duration::from_secs(10))?;

    // Verify we can execute a simple query
    let mut redis = db
      .pool
      .get_timeout(Duration::from_secs(10))
      .context("Failed to get redis pool instance")?;

    redis
      .set::<&str, &str, ()>("test", "test")
      .context("Failed to set test value in Redis")?;

    Ok(())
  }

  #[tokio::test]
  async fn test_invalid_connection() {
    let result = Cache::connect("redis://invalid", 5, Duration::from_secs(5));
    assert!(result.is_err());
  }
}