Skip to main content

redis_oxide/
pool.rs

1//! Connection pooling implementations
2//!
3//! This module provides two strategies for managing Redis connections:
4//! - Multiplexed: Single connection shared across multiple tasks
5//! - Pool: Multiple connections managed in a pool
6
7use crate::connection::RedisConnection;
8use crate::core::{
9    config::{ConnectionConfig, PoolStrategy},
10    error::{RedisError, RedisResult},
11    value::RespValue,
12};
13use std::sync::Arc;
14use tokio::sync::{mpsc, Mutex, RwLock, Semaphore};
15use tracing::{debug, warn};
16
17/// Request to execute a command through the multiplexed connection
18struct CommandRequest {
19    command: String,
20    args: Vec<RespValue>,
21    response_tx: tokio::sync::oneshot::Sender<RedisResult<RespValue>>,
22}
23
24/// Multiplexed connection pool - uses a single connection with mpsc channel
25pub struct MultiplexedPool {
26    command_tx: mpsc::UnboundedSender<CommandRequest>,
27}
28
29impl MultiplexedPool {
30    /// Create a new multiplexed pool
31    pub async fn new(config: ConnectionConfig, host: String, port: u16) -> RedisResult<Self> {
32        let (command_tx, mut command_rx) = mpsc::unbounded_channel::<CommandRequest>();
33
34        // Spawn background task to handle commands
35        tokio::spawn(async move {
36            let mut conn = match RedisConnection::connect(&host, port, config.clone()).await {
37                Ok(conn) => conn,
38                Err(e) => {
39                    warn!("Failed to create multiplexed connection: {:?}", e);
40                    return;
41                }
42            };
43
44            while let Some(req) = command_rx.recv().await {
45                let result = conn.execute_command(&req.command, &req.args).await;
46                // Ignore send errors - client may have dropped the receiver
47                let _ = req.response_tx.send(result);
48            }
49
50            debug!("Multiplexed connection handler stopped");
51        });
52
53        Ok(Self { command_tx })
54    }
55
56    /// Execute a command through the multiplexed connection
57    pub async fn execute_command(
58        &self,
59        command: String,
60        args: Vec<RespValue>,
61    ) -> RedisResult<RespValue> {
62        let (response_tx, response_rx) = tokio::sync::oneshot::channel();
63
64        self.command_tx
65            .send(CommandRequest {
66                command,
67                args,
68                response_tx,
69            })
70            .map_err(|_| RedisError::Connection("Multiplexed connection closed".to_string()))?;
71
72        response_rx
73            .await
74            .map_err(|_| RedisError::Connection("Response channel closed".to_string()))?
75    }
76}
77
78/// Traditional connection pool with multiple connections
79pub struct ConnectionPool {
80    connections: Arc<RwLock<Vec<Arc<Mutex<RedisConnection>>>>>,
81    semaphore: Arc<Semaphore>,
82    config: ConnectionConfig,
83    host: String,
84    port: u16,
85}
86
87impl ConnectionPool {
88    /// Create a new connection pool
89    pub async fn new(
90        config: ConnectionConfig,
91        host: String,
92        port: u16,
93        max_size: usize,
94    ) -> RedisResult<Self> {
95        let mut connections = Vec::new();
96
97        // Create initial connections (at least 1)
98        let initial_size = config.pool.min_idle.min(max_size).max(1);
99        for _ in 0..initial_size {
100            let conn = RedisConnection::connect(&host, port, config.clone()).await?;
101            connections.push(Arc::new(Mutex::new(conn)));
102        }
103
104        Ok(Self {
105            connections: Arc::new(RwLock::new(connections)),
106            semaphore: Arc::new(Semaphore::new(max_size)),
107            config,
108            host,
109            port,
110        })
111    }
112
113    /// Get a connection from the pool
114    async fn get_connection(&self) -> RedisResult<Arc<Mutex<RedisConnection>>> {
115        // Acquire semaphore permit
116        let _permit = self
117            .semaphore
118            .acquire()
119            .await
120            .map_err(|_| RedisError::Pool("Failed to acquire permit".to_string()))?;
121
122        // Try to get an existing connection
123        {
124            let mut connections = self.connections.write().await;
125            if let Some(conn) = connections.pop() {
126                return Ok(conn);
127            }
128        }
129
130        // Create a new connection if none available
131        let conn = RedisConnection::connect(&self.host, self.port, self.config.clone()).await?;
132        Ok(Arc::new(Mutex::new(conn)))
133    }
134
135    /// Return a connection to the pool
136    async fn return_connection(&self, conn: Arc<Mutex<RedisConnection>>) {
137        let mut connections = self.connections.write().await;
138        connections.push(conn);
139    }
140
141    /// Execute a command using a connection from the pool
142    pub async fn execute_command(
143        &self,
144        command: String,
145        args: Vec<RespValue>,
146    ) -> RedisResult<RespValue> {
147        let conn = self.get_connection().await?;
148        let result = {
149            let mut conn_guard = conn.lock().await;
150            conn_guard.execute_command(&command, &args).await
151        };
152
153        // Return connection to pool
154        self.return_connection(conn).await;
155
156        result
157    }
158}
159
160/// Unified pool abstraction that can be either multiplexed or traditional pool
161pub enum Pool {
162    /// Multiplexed connection
163    Multiplexed(MultiplexedPool),
164    /// Traditional connection pool
165    Pool(Box<ConnectionPool>),
166}
167
168impl Pool {
169    /// Create a new pool based on the configuration
170    pub async fn new(config: ConnectionConfig, host: String, port: u16) -> RedisResult<Self> {
171        match config.pool.strategy {
172            PoolStrategy::Multiplexed => {
173                let pool = MultiplexedPool::new(config, host, port).await?;
174                Ok(Self::Multiplexed(pool))
175            }
176            PoolStrategy::Pool => {
177                let pool =
178                    ConnectionPool::new(config.clone(), host, port, config.pool.max_size).await?;
179                Ok(Self::Pool(Box::new(pool)))
180            }
181        }
182    }
183
184    /// Execute a command through the pool
185    pub async fn execute_command(
186        &self,
187        command: String,
188        args: Vec<RespValue>,
189    ) -> RedisResult<RespValue> {
190        match self {
191            Self::Multiplexed(pool) => pool.execute_command(command, args).await,
192            Self::Pool(pool) => pool.execute_command(command, args).await,
193        }
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use crate::core::config::PoolConfig;
201
202    #[test]
203    fn test_pool_config() {
204        let config = ConnectionConfig::new("redis://localhost:6379");
205        assert_eq!(config.pool.strategy, PoolStrategy::Multiplexed);
206    }
207
208    #[test]
209    fn test_custom_pool_config() {
210        let mut config = ConnectionConfig::new("redis://localhost:6379");
211        config.pool = PoolConfig {
212            strategy: PoolStrategy::Pool,
213            max_size: 20,
214            min_idle: 5,
215            ..Default::default()
216        };
217
218        assert_eq!(config.pool.strategy, PoolStrategy::Pool);
219        assert_eq!(config.pool.max_size, 20);
220    }
221}