Skip to main content

deadpool_postgres/
config.rs

1//! Configuration used for [`Pool`] creation.
2
3use std::{env, fmt, net::IpAddr, str::FromStr, time::Duration};
4
5use tokio_postgres::config::{
6    ChannelBinding as PgChannelBinding, LoadBalanceHosts as PgLoadBalanceHosts,
7    SslMode as PgSslMode, TargetSessionAttrs as PgTargetSessionAttrs,
8};
9
10#[cfg(not(target_arch = "wasm32"))]
11use super::Pool;
12#[cfg(not(target_arch = "wasm32"))]
13use crate::{CreatePoolError, PoolBuilder, Runtime};
14#[cfg(not(target_arch = "wasm32"))]
15use tokio_postgres::{
16    Socket,
17    tls::{MakeTlsConnect, TlsConnect},
18};
19
20use super::PoolConfig;
21
22/// Configuration object.
23///
24/// # Example (from environment)
25///
26/// By enabling the `serde` feature you can read the configuration using the
27/// [`config`](https://crates.io/crates/config) crate as following:
28/// ```env
29/// PG__HOST=pg.example.com
30/// PG__USER=john_doe
31/// PG__PASSWORD=topsecret
32/// PG__DBNAME=example
33/// PG__POOL__MAX_SIZE=16
34/// PG__POOL__TIMEOUTS__WAIT__SECS=5
35/// PG__POOL__TIMEOUTS__WAIT__NANOS=0
36/// ```
37/// ```rust
38/// #[derive(serde::Deserialize, serde::Serialize)]
39/// struct Config {
40///     pg: deadpool_postgres::Config,
41/// }
42/// impl Config {
43///     pub fn from_env() -> Result<Self, config::ConfigError> {
44///         let mut cfg = config::Config::builder()
45///            .add_source(config::Environment::default().separator("__"))
46///            .build()?;
47///            cfg.try_deserialize()
48///     }
49/// }
50/// ```
51#[derive(Clone, Debug, Default)]
52#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
53pub struct Config {
54    /// Initialize the configuration by parsing the URL first.
55    /// **Note**: All the other options override settings defined
56    /// by the URL except for the `host` and `hosts` options which
57    /// are additive!
58    pub url: Option<String>,
59    /// See [`tokio_postgres::Config::user`].
60    pub user: Option<String>,
61    /// See [`tokio_postgres::Config::password`].
62    pub password: Option<String>,
63    /// See [`tokio_postgres::Config::dbname`].
64    pub dbname: Option<String>,
65    /// See [`tokio_postgres::Config::options`].
66    pub options: Option<String>,
67    /// See [`tokio_postgres::Config::application_name`].
68    pub application_name: Option<String>,
69    /// See [`tokio_postgres::Config::ssl_mode`].
70    pub ssl_mode: Option<SslMode>,
71    /// This is similar to [`Config::hosts`] but only allows one host to be
72    /// specified.
73    ///
74    /// Unlike [`tokio_postgres::Config`] this structure differentiates between
75    /// one host and more than one host. This makes it possible to store this
76    /// configuration in an environment variable.
77    ///
78    /// See [`tokio_postgres::Config::host`].
79    pub host: Option<String>,
80    /// See [`tokio_postgres::Config::host`].
81    pub hosts: Option<Vec<String>>,
82    /// See [`tokio_postgres::Config::hostaddr`].
83    pub hostaddr: Option<IpAddr>,
84    /// See [`tokio_postgres::Config::hostaddr`].
85    pub hostaddrs: Option<Vec<IpAddr>>,
86    /// This is similar to [`Config::ports`] but only allows one port to be
87    /// specified.
88    ///
89    /// Unlike [`tokio_postgres::Config`] this structure differentiates between
90    /// one port and more than one port. This makes it possible to store this
91    /// configuration in an environment variable.
92    ///
93    /// See [`tokio_postgres::Config::port`].
94    pub port: Option<u16>,
95    /// See [`tokio_postgres::Config::port`].
96    pub ports: Option<Vec<u16>>,
97    /// See [`tokio_postgres::Config::connect_timeout`].
98    pub connect_timeout: Option<Duration>,
99    /// See [`tokio_postgres::Config::keepalives`].
100    pub keepalives: Option<bool>,
101    #[cfg(not(target_arch = "wasm32"))]
102    /// See [`tokio_postgres::Config::keepalives_idle`].
103    pub keepalives_idle: Option<Duration>,
104    /// See [`tokio_postgres::Config::target_session_attrs`].
105    pub target_session_attrs: Option<TargetSessionAttrs>,
106    /// See [`tokio_postgres::Config::channel_binding`].
107    pub channel_binding: Option<ChannelBinding>,
108    /// See [`tokio_postgres::Config::load_balance_hosts`].
109    pub load_balance_hosts: Option<LoadBalanceHosts>,
110
111    /// [`Manager`] configuration.
112    ///
113    /// [`Manager`]: super::Manager
114    pub manager: Option<ManagerConfig>,
115
116    /// [`Pool`] configuration.
117    pub pool: Option<PoolConfig>,
118}
119
120/// This error is returned if there is something wrong with the configuration
121#[derive(Debug)]
122pub enum ConfigError {
123    /// This variant is returned if the `url` is invalid
124    InvalidUrl(tokio_postgres::Error),
125    /// This variant is returned if the `dbname` is missing from the config
126    DbnameMissing,
127    /// This variant is returned if the `dbname` contains an empty string
128    DbnameEmpty,
129}
130
131impl fmt::Display for ConfigError {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        match self {
134            Self::InvalidUrl(e) => write!(f, "configuration property \"url\" is invalid: {e}"),
135            Self::DbnameMissing => write!(f, "configuration property \"dbname\" not found"),
136            Self::DbnameEmpty => write!(
137                f,
138                "configuration property \"dbname\" contains an empty string",
139            ),
140        }
141    }
142}
143
144impl std::error::Error for ConfigError {}
145
146impl Config {
147    /// Create a new [`Config`] instance with default values. This function is
148    /// identical to [`Config::default()`].
149    #[must_use]
150    pub fn new() -> Self {
151        Self::default()
152    }
153
154    #[cfg(not(target_arch = "wasm32"))]
155    /// Creates a new [`Pool`] using this [`Config`].
156    ///
157    /// # Errors
158    ///
159    /// See [`CreatePoolError`] for details.
160    pub fn create_pool<T>(&self, runtime: Option<Runtime>, tls: T) -> Result<Pool, CreatePoolError>
161    where
162        T: MakeTlsConnect<Socket> + Clone + Sync + Send + 'static,
163        T::Stream: Sync + Send,
164        T::TlsConnect: Sync + Send,
165        <T::TlsConnect as TlsConnect<Socket>>::Future: Send,
166    {
167        let mut builder = self.builder(tls).map_err(CreatePoolError::Config)?;
168        if let Some(runtime) = runtime {
169            builder = builder.runtime(runtime);
170        }
171        builder.build().map_err(CreatePoolError::Build)
172    }
173
174    #[cfg(not(target_arch = "wasm32"))]
175    /// Creates a new [`PoolBuilder`] using this [`Config`].
176    ///
177    /// # Errors
178    ///
179    /// See [`ConfigError`] and [`tokio_postgres::Error`] for details.
180    pub fn builder<T>(&self, tls: T) -> Result<PoolBuilder, ConfigError>
181    where
182        T: MakeTlsConnect<Socket> + Clone + Sync + Send + 'static,
183        T::Stream: Sync + Send,
184        T::TlsConnect: Sync + Send,
185        <T::TlsConnect as TlsConnect<Socket>>::Future: Send,
186    {
187        let pg_config = self.get_pg_config()?;
188        let manager_config = self.get_manager_config();
189        let manager = crate::Manager::from_config(pg_config, tls, manager_config);
190        let pool_config = self.get_pool_config();
191        Ok(Pool::builder(manager).config(pool_config))
192    }
193
194    /// Returns [`tokio_postgres::Config`] which can be used to connect to
195    /// the database.
196    #[allow(unused_results)]
197    pub fn get_pg_config(&self) -> Result<tokio_postgres::Config, ConfigError> {
198        let mut cfg = if let Some(url) = &self.url {
199            tokio_postgres::Config::from_str(url).map_err(ConfigError::InvalidUrl)?
200        } else {
201            tokio_postgres::Config::new()
202        };
203        if let Some(user) = self.user.as_ref().filter(|s| !s.is_empty()) {
204            cfg.user(user.as_str());
205        }
206        if cfg.get_user().is_none_or(|u| u.is_empty()) {
207            if let Ok(user) = env::var("USER") {
208                cfg.user(&user);
209            }
210        }
211        if let Some(password) = &self.password {
212            cfg.password(password);
213        }
214        if let Some(dbname) = self.dbname.as_ref().filter(|s| !s.is_empty()) {
215            cfg.dbname(dbname);
216        }
217        match cfg.get_dbname() {
218            None => {
219                return Err(ConfigError::DbnameMissing);
220            }
221            Some("") => {
222                return Err(ConfigError::DbnameEmpty);
223            }
224            _ => {}
225        }
226        if let Some(options) = &self.options {
227            cfg.options(options.as_str());
228        }
229        if let Some(application_name) = &self.application_name {
230            cfg.application_name(application_name.as_str());
231        }
232        if let Some(host) = &self.host {
233            cfg.host(host.as_str());
234        }
235        if let Some(hosts) = &self.hosts {
236            for host in hosts.iter() {
237                cfg.host(host.as_str());
238            }
239        }
240        if cfg.get_hosts().is_empty() {
241            // Systems that support it default to unix domain sockets.
242            #[cfg(unix)]
243            {
244                cfg.host_path("/run/postgresql");
245                cfg.host_path("/var/run/postgresql");
246                cfg.host_path("/tmp");
247            }
248            // Windows and other systems use 127.0.0.1 instead.
249            #[cfg(not(unix))]
250            cfg.host("127.0.0.1");
251        }
252        if let Some(hostaddr) = self.hostaddr {
253            cfg.hostaddr(hostaddr);
254        }
255        if let Some(hostaddrs) = &self.hostaddrs {
256            for hostaddr in hostaddrs {
257                cfg.hostaddr(*hostaddr);
258            }
259        }
260        if let Some(port) = self.port {
261            cfg.port(port);
262        }
263        if let Some(ports) = &self.ports {
264            for port in ports.iter() {
265                cfg.port(*port);
266            }
267        }
268        if let Some(connect_timeout) = self.connect_timeout {
269            cfg.connect_timeout(connect_timeout);
270        }
271        if let Some(keepalives) = self.keepalives {
272            cfg.keepalives(keepalives);
273        }
274        #[cfg(not(target_arch = "wasm32"))]
275        if let Some(keepalives_idle) = self.keepalives_idle {
276            cfg.keepalives_idle(keepalives_idle);
277        }
278        if let Some(mode) = self.ssl_mode {
279            cfg.ssl_mode(mode.into());
280        }
281        Ok(cfg)
282    }
283
284    /// Returns [`ManagerConfig`] which can be used to construct a
285    /// [`deadpool::managed::Pool`] instance.
286    #[must_use]
287    pub fn get_manager_config(&self) -> ManagerConfig {
288        self.manager.clone().unwrap_or_default()
289    }
290
291    /// Returns [`deadpool::managed::PoolConfig`] which can be used to construct
292    /// a [`deadpool::managed::Pool`] instance.
293    #[must_use]
294    pub fn get_pool_config(&self) -> PoolConfig {
295        self.pool.unwrap_or_default()
296    }
297}
298
299/// Possible methods of how a connection is recycled.
300///
301/// The default is [`Fast`] which does not check the connection health or
302/// perform any clean-up queries.
303///
304/// [`Fast`]: RecyclingMethod::Fast
305/// [`Verified`]: RecyclingMethod::Verified
306#[derive(Clone, Debug, Eq, PartialEq, Default)]
307#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
308pub enum RecyclingMethod {
309    /// Only run [`Client::is_closed()`][1] when recycling existing connections.
310    ///
311    /// Unless you have special needs this is a safe choice.
312    ///
313    /// [1]: tokio_postgres::Client::is_closed
314    #[default]
315    Fast,
316
317    /// Run [`Client::is_closed()`][1] and execute a test query.
318    ///
319    /// This is slower, but guarantees that the database connection is ready to
320    /// be used. Normally, [`Client::is_closed()`][1] should be enough to filter
321    /// out bad connections, but under some circumstances (i.e. hard-closed
322    /// network connections) it's possible that [`Client::is_closed()`][1]
323    /// returns `false` while the connection is dead. You will receive an error
324    /// on your first query then.
325    ///
326    /// [1]: tokio_postgres::Client::is_closed
327    Verified,
328
329    /// Like [`Verified`] query method, but instead use the following sequence
330    /// of statements which guarantees a pristine connection:
331    /// ```sql
332    /// CLOSE ALL;
333    /// SET SESSION AUTHORIZATION DEFAULT;
334    /// RESET ALL;
335    /// UNLISTEN *;
336    /// SELECT pg_advisory_unlock_all();
337    /// DISCARD TEMP;
338    /// DISCARD SEQUENCES;
339    /// ```
340    ///
341    /// This is similar to calling `DISCARD ALL`. but doesn't call
342    /// `DEALLOCATE ALL` and `DISCARD PLAN`, so that the statement cache is not
343    /// rendered ineffective.
344    ///
345    /// [`Verified`]: RecyclingMethod::Verified
346    Clean,
347
348    /// Like [`Verified`] but allows to specify a custom SQL to be executed.
349    ///
350    /// [`Verified`]: RecyclingMethod::Verified
351    Custom(String),
352}
353
354impl RecyclingMethod {
355    const DISCARD_SQL: &'static str = "\
356        CLOSE ALL; \
357        SET SESSION AUTHORIZATION DEFAULT; \
358        RESET ALL; \
359        UNLISTEN *; \
360        SELECT pg_advisory_unlock_all(); \
361        DISCARD TEMP; \
362        DISCARD SEQUENCES;\
363    ";
364
365    /// Returns SQL query to be executed when recycling a connection.
366    pub fn query(&self) -> Option<&str> {
367        match self {
368            Self::Fast => None,
369            Self::Verified => Some(""),
370            Self::Clean => Some(Self::DISCARD_SQL),
371            Self::Custom(sql) => Some(sql),
372        }
373    }
374}
375
376/// Configuration object for a [`Manager`].
377///
378/// This currently only makes it possible to specify which [`RecyclingMethod`]
379/// should be used when retrieving existing objects from the [`Pool`].
380///
381/// [`Manager`]: super::Manager
382#[derive(Clone, Debug, Default)]
383#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
384pub struct ManagerConfig {
385    /// Method of how a connection is recycled. See [`RecyclingMethod`].
386    pub recycling_method: RecyclingMethod,
387}
388
389/// Properties required of a session.
390///
391/// This is a 1:1 copy of the [`PgTargetSessionAttrs`] enumeration.
392/// This is duplicated here in order to add support for the
393/// [`serde::Deserialize`] trait which is required for the [`serde`] support.
394#[derive(Clone, Copy, Debug, Eq, PartialEq)]
395#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
396#[non_exhaustive]
397pub enum TargetSessionAttrs {
398    /// No special properties are required.
399    Any,
400
401    /// The session must allow writes.
402    ReadWrite,
403}
404
405impl From<TargetSessionAttrs> for PgTargetSessionAttrs {
406    fn from(attrs: TargetSessionAttrs) -> Self {
407        match attrs {
408            TargetSessionAttrs::Any => Self::Any,
409            TargetSessionAttrs::ReadWrite => Self::ReadWrite,
410        }
411    }
412}
413
414/// TLS configuration.
415///
416/// This is a 1:1 copy of the [`PgSslMode`] enumeration.
417/// This is duplicated here in order to add support for the
418/// [`serde::Deserialize`] trait which is required for the [`serde`] support.
419#[derive(Clone, Copy, Debug, Eq, PartialEq)]
420#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
421#[non_exhaustive]
422pub enum SslMode {
423    /// Do not use TLS.
424    Disable,
425
426    /// Attempt to connect with TLS but allow sessions without.
427    Prefer,
428
429    /// Require the use of TLS.
430    Require,
431}
432
433impl From<SslMode> for PgSslMode {
434    fn from(mode: SslMode) -> Self {
435        match mode {
436            SslMode::Disable => Self::Disable,
437            SslMode::Prefer => Self::Prefer,
438            SslMode::Require => Self::Require,
439        }
440    }
441}
442
443/// Channel binding configuration.
444///
445/// This is a 1:1 copy of the [`PgChannelBinding`] enumeration.
446/// This is duplicated here in order to add support for the
447/// [`serde::Deserialize`] trait which is required for the [`serde`] support.
448#[derive(Clone, Copy, Debug, Eq, PartialEq)]
449#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
450#[non_exhaustive]
451pub enum ChannelBinding {
452    /// Do not use channel binding.
453    Disable,
454
455    /// Attempt to use channel binding but allow sessions without.
456    Prefer,
457
458    /// Require the use of channel binding.
459    Require,
460}
461
462impl From<ChannelBinding> for PgChannelBinding {
463    fn from(cb: ChannelBinding) -> Self {
464        match cb {
465            ChannelBinding::Disable => Self::Disable,
466            ChannelBinding::Prefer => Self::Prefer,
467            ChannelBinding::Require => Self::Require,
468        }
469    }
470}
471
472/// Load balancing configuration.
473///
474/// This is a 1:1 copy of the [`PgLoadBalanceHosts`] enumeration.
475/// This is duplicated here in order to add support for the
476/// [`serde::Deserialize`] trait which is required for the [`serde`] support.
477#[derive(Debug, Copy, Clone, PartialEq, Eq)]
478#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
479#[non_exhaustive]
480pub enum LoadBalanceHosts {
481    /// Make connection attempts to hosts in the order provided.
482    Disable,
483    /// Make connection attempts to hosts in a random order.
484    Random,
485}
486
487impl From<LoadBalanceHosts> for PgLoadBalanceHosts {
488    fn from(cb: LoadBalanceHosts) -> Self {
489        match cb {
490            LoadBalanceHosts::Disable => Self::Disable,
491            LoadBalanceHosts::Random => Self::Random,
492        }
493    }
494}