deadpool_oracle/
lib.rs

1//! Deadpool connection pool for Oracle databases
2//!
3//! This crate provides a connection pool for the `oracle-rs` driver using the
4//! `deadpool` async pool library.
5//!
6//! # Example
7//!
8//! ```rust,no_run
9//! use oracle_rs::Config;
10//! use deadpool_oracle::{Pool, PoolBuilder};
11//!
12//! #[tokio::main]
13//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
14//!     // Create connection config
15//!     let config = Config::new("localhost", 1521, "FREEPDB1", "user", "password");
16//!
17//!     // Create pool
18//!     let pool = PoolBuilder::new(config)
19//!         .max_size(10)
20//!         .build()?;
21//!
22//!     // Get a connection from the pool
23//!     let conn = pool.get().await?;
24//!
25//!     // Use the connection
26//!     let result = conn.query("SELECT * FROM users", &[]).await?;
27//!     println!("Found {} rows", result.row_count());
28//!
29//!     // Connection is automatically returned to the pool when dropped
30//!     Ok(())
31//! }
32//! ```
33
34use deadpool::managed::{self, Manager, Metrics, RecycleError, RecycleResult};
35use oracle_rs::{Config, Connection, Error};
36use std::time::Duration;
37
38/// Manager for creating and recycling Oracle connections
39///
40/// This implements the `deadpool::managed::Manager` trait to integrate
41/// with the deadpool connection pool.
42pub struct OracleConnectionManager {
43    config: Config,
44}
45
46impl OracleConnectionManager {
47    /// Create a new connection manager with the given configuration
48    pub fn new(config: Config) -> Self {
49        Self { config }
50    }
51}
52
53impl Manager for OracleConnectionManager {
54    type Type = Connection;
55    type Error = Error;
56
57    async fn create(&self) -> Result<Connection, Error> {
58        Connection::connect_with_config(self.config.clone()).await
59    }
60
61    async fn recycle(
62        &self,
63        conn: &mut Connection,
64        _metrics: &Metrics,
65    ) -> RecycleResult<Error> {
66        // Check if connection is still alive
67        if conn.is_closed() {
68            return Err(RecycleError::message("connection closed"));
69        }
70
71        // Rollback any pending transaction to ensure clean state
72        conn.rollback().await.ok();
73
74        // Verify connection still works
75        conn.ping().await.map_err(RecycleError::Backend)?;
76
77        Ok(())
78    }
79}
80
81/// Type alias for the connection pool
82pub type Pool = managed::Pool<OracleConnectionManager>;
83
84/// Type alias for a pooled connection
85///
86/// This wraps a `Connection` and automatically returns it to the pool when dropped.
87pub type Object = managed::Object<OracleConnectionManager>;
88
89/// Builder for creating connection pools with custom configuration
90///
91/// # Example
92///
93/// ```rust,no_run
94/// use oracle_rs::Config;
95/// use deadpool_oracle::PoolBuilder;
96/// use std::time::Duration;
97///
98/// let config = Config::new("localhost", 1521, "FREEPDB1", "user", "password");
99/// let pool = PoolBuilder::new(config)
100///     .max_size(20)
101///     .wait_timeout(Some(Duration::from_secs(30)))
102///     .build()
103///     .expect("Failed to build pool");
104/// ```
105pub struct PoolBuilder {
106    config: Config,
107    max_size: usize,
108    wait_timeout: Option<Duration>,
109    create_timeout: Option<Duration>,
110    recycle_timeout: Option<Duration>,
111}
112
113impl PoolBuilder {
114    /// Create a new pool builder with the given connection configuration
115    pub fn new(config: Config) -> Self {
116        Self {
117            config,
118            max_size: num_cpus() * 4,
119            wait_timeout: Some(Duration::from_secs(30)),
120            create_timeout: Some(Duration::from_secs(30)),
121            recycle_timeout: Some(Duration::from_secs(5)),
122        }
123    }
124
125    /// Set the maximum number of connections in the pool
126    ///
127    /// Default is `num_cpus * 4`.
128    pub fn max_size(mut self, size: usize) -> Self {
129        self.max_size = size;
130        self
131    }
132
133    /// Set the timeout for waiting for a connection from the pool
134    ///
135    /// If the pool is exhausted, this is how long to wait before returning an error.
136    /// Default is 30 seconds. Set to `None` to wait indefinitely.
137    pub fn wait_timeout(mut self, timeout: Option<Duration>) -> Self {
138        self.wait_timeout = timeout;
139        self
140    }
141
142    /// Set the timeout for creating a new connection
143    ///
144    /// Default is 30 seconds. Set to `None` to wait indefinitely.
145    pub fn create_timeout(mut self, timeout: Option<Duration>) -> Self {
146        self.create_timeout = timeout;
147        self
148    }
149
150    /// Set the timeout for recycling a connection (health check)
151    ///
152    /// Default is 5 seconds. Set to `None` to wait indefinitely.
153    pub fn recycle_timeout(mut self, timeout: Option<Duration>) -> Self {
154        self.recycle_timeout = timeout;
155        self
156    }
157
158    /// Build the connection pool
159    ///
160    /// This creates the pool but does not establish any connections.
161    /// Connections are created lazily when first requested.
162    pub fn build(self) -> Result<Pool, BuildError> {
163        let manager = OracleConnectionManager::new(self.config);
164
165        let builder = managed::Pool::builder(manager)
166            .max_size(self.max_size)
167            .runtime(deadpool::Runtime::Tokio1)
168            .timeouts(managed::Timeouts {
169                wait: self.wait_timeout,
170                create: self.create_timeout,
171                recycle: self.recycle_timeout,
172            });
173
174        builder.build().map_err(BuildError)
175    }
176}
177
178/// Error that can occur when building a connection pool
179#[derive(Debug)]
180pub struct BuildError(managed::BuildError);
181
182impl std::fmt::Display for BuildError {
183    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        write!(f, "failed to build connection pool: {}", self.0)
185    }
186}
187
188impl std::error::Error for BuildError {
189    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
190        Some(&self.0)
191    }
192}
193
194/// Error that can occur when getting a connection from the pool
195pub type PoolError = managed::PoolError<Error>;
196
197/// Helper to get CPU count for default pool size
198fn num_cpus() -> usize {
199    std::thread::available_parallelism()
200        .map(|p| p.get())
201        .unwrap_or(4)
202}
203
204/// Extension trait for creating pools directly from Config
205pub trait ConfigExt {
206    /// Create a connection pool with default configuration
207    fn into_pool(self) -> Result<Pool, BuildError>;
208
209    /// Create a connection pool with the specified maximum size
210    fn into_pool_with_size(self, max_size: usize) -> Result<Pool, BuildError>;
211}
212
213impl ConfigExt for Config {
214    fn into_pool(self) -> Result<Pool, BuildError> {
215        PoolBuilder::new(self).build()
216    }
217
218    fn into_pool_with_size(self, max_size: usize) -> Result<Pool, BuildError> {
219        PoolBuilder::new(self).max_size(max_size).build()
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_pool_builder_defaults() {
229        let config = Config::new("localhost", 1521, "FREEPDB1", "test", "test");
230        let builder = PoolBuilder::new(config);
231
232        assert!(builder.max_size > 0);
233        assert!(builder.wait_timeout.is_some());
234        assert!(builder.create_timeout.is_some());
235        assert!(builder.recycle_timeout.is_some());
236    }
237
238    #[test]
239    fn test_pool_builder_configuration() {
240        let config = Config::new("localhost", 1521, "FREEPDB1", "test", "test");
241        let builder = PoolBuilder::new(config)
242            .max_size(5)
243            .wait_timeout(Some(Duration::from_secs(10)))
244            .create_timeout(None)
245            .recycle_timeout(Some(Duration::from_secs(2)));
246
247        assert_eq!(builder.max_size, 5);
248        assert_eq!(builder.wait_timeout, Some(Duration::from_secs(10)));
249        assert_eq!(builder.create_timeout, None);
250        assert_eq!(builder.recycle_timeout, Some(Duration::from_secs(2)));
251    }
252
253    #[test]
254    fn test_pool_build_lazy() {
255        let config = Config::new("localhost", 1521, "FREEPDB1", "test", "test");
256        let pool = PoolBuilder::new(config).max_size(10).build();
257
258        // Pool creation should succeed (connections are lazy)
259        assert!(pool.is_ok());
260
261        let pool = pool.unwrap();
262        let status = pool.status();
263
264        // No connections yet (lazy)
265        assert_eq!(status.size, 0);
266        assert_eq!(status.available, 0);
267    }
268}