lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

config.rs (6514B)


      1 //! Validated connection limits for one service-owned SQLite database.
      2 
      3 use core::{fmt, time::Duration};
      4 use std::error::Error;
      5 
      6 const MIN_BUSY_TIMEOUT: Duration = Duration::from_millis(1);
      7 const DEFAULT_BUSY_TIMEOUT: Duration = Duration::from_secs(5);
      8 const MAX_BUSY_TIMEOUT: Duration = Duration::from_secs(60);
      9 const MIN_CONNECTIONS: u32 = 1;
     10 const DEFAULT_MAX_CONNECTIONS: u32 = 8;
     11 const MAX_CONNECTIONS: u32 = 8;
     12 
     13 /// Validated limits applied to every connection in a service SQLite pool.
     14 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     15 pub struct ServiceSqliteConnectionOptions {
     16     busy_timeout: Duration,
     17     max_connections: u32,
     18 }
     19 
     20 impl ServiceSqliteConnectionOptions {
     21     /// Returns the reviewed service defaults: five seconds and eight connections.
     22     #[must_use]
     23     pub const fn reviewed() -> Self {
     24         Self {
     25             busy_timeout: DEFAULT_BUSY_TIMEOUT,
     26             max_connections: DEFAULT_MAX_CONNECTIONS,
     27         }
     28     }
     29 
     30     /// Validates an integral-millisecond busy timeout and bounded pool size.
     31     pub fn new(
     32         busy_timeout: Duration,
     33         max_connections: u32,
     34     ) -> Result<Self, ServiceSqliteConnectionOptionsError> {
     35         let timeout_nanos = busy_timeout.as_nanos();
     36         if !timeout_nanos.is_multiple_of(1_000_000) {
     37             return Err(ServiceSqliteConnectionOptionsError::BusyTimeoutNotMilliseconds);
     38         }
     39         if busy_timeout < MIN_BUSY_TIMEOUT {
     40             return Err(ServiceSqliteConnectionOptionsError::BusyTimeoutTooSmall);
     41         }
     42         if busy_timeout > MAX_BUSY_TIMEOUT {
     43             return Err(ServiceSqliteConnectionOptionsError::BusyTimeoutTooLarge);
     44         }
     45         if max_connections < MIN_CONNECTIONS {
     46             return Err(ServiceSqliteConnectionOptionsError::PoolTooSmall);
     47         }
     48         if max_connections > MAX_CONNECTIONS {
     49             return Err(ServiceSqliteConnectionOptionsError::PoolTooLarge);
     50         }
     51         Ok(Self {
     52             busy_timeout,
     53             max_connections,
     54         })
     55     }
     56 
     57     /// Returns the exact busy timeout applied to SQLite and pool acquisition.
     58     #[must_use]
     59     pub const fn busy_timeout(self) -> Duration {
     60         self.busy_timeout
     61     }
     62 
     63     /// Returns the maximum number of connections in the pool.
     64     #[must_use]
     65     pub const fn max_connections(self) -> u32 {
     66         self.max_connections
     67     }
     68 
     69     #[allow(
     70         dead_code,
     71         reason = "Step 056 keeps pragma verification private until the Step 061 host boundary"
     72     )]
     73     pub(crate) fn busy_timeout_milliseconds(self) -> i64 {
     74         i64::try_from(self.busy_timeout.as_millis())
     75             .expect("validated busy timeout always fits in i64 milliseconds")
     76     }
     77 }
     78 
     79 impl Default for ServiceSqliteConnectionOptions {
     80     fn default() -> Self {
     81         Self::reviewed()
     82     }
     83 }
     84 
     85 /// Path-free failure returned when SQLite connection limits are invalid.
     86 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     87 pub enum ServiceSqliteConnectionOptionsError {
     88     BusyTimeoutTooSmall,
     89     BusyTimeoutTooLarge,
     90     BusyTimeoutNotMilliseconds,
     91     PoolTooSmall,
     92     PoolTooLarge,
     93 }
     94 
     95 impl fmt::Display for ServiceSqliteConnectionOptionsError {
     96     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     97         formatter.write_str(match self {
     98             Self::BusyTimeoutTooSmall => "SQLite busy timeout must be at least one millisecond",
     99             Self::BusyTimeoutTooLarge => "SQLite busy timeout must not exceed sixty seconds",
    100             Self::BusyTimeoutNotMilliseconds => {
    101                 "SQLite busy timeout must be an integral number of milliseconds"
    102             }
    103             Self::PoolTooSmall => "SQLite connection pool must contain at least one connection",
    104             Self::PoolTooLarge => "SQLite connection pool must not exceed eight connections",
    105         })
    106     }
    107 }
    108 
    109 impl Error for ServiceSqliteConnectionOptionsError {}
    110 
    111 #[cfg(test)]
    112 mod tests {
    113     use super::*;
    114 
    115     #[test]
    116     fn reviewed_defaults_and_complete_boundary_inventory_are_exact() {
    117         let reviewed = ServiceSqliteConnectionOptions::reviewed();
    118         assert_eq!(reviewed.busy_timeout(), Duration::from_secs(5));
    119         assert_eq!(reviewed.max_connections(), 8);
    120         assert_eq!(ServiceSqliteConnectionOptions::default(), reviewed);
    121 
    122         for timeout in [
    123             Duration::from_millis(1),
    124             Duration::from_secs(5),
    125             Duration::from_secs(60),
    126         ] {
    127             for max_connections in 1..=8 {
    128                 assert_eq!(
    129                     ServiceSqliteConnectionOptions::new(timeout, max_connections),
    130                     Ok(ServiceSqliteConnectionOptions {
    131                         busy_timeout: timeout,
    132                         max_connections,
    133                     })
    134                 );
    135             }
    136         }
    137     }
    138 
    139     #[test]
    140     fn zero_fractional_oversized_and_extreme_values_fail_closed() {
    141         let vectors = [
    142             (
    143                 Duration::ZERO,
    144                 8,
    145                 ServiceSqliteConnectionOptionsError::BusyTimeoutTooSmall,
    146             ),
    147             (
    148                 Duration::from_nanos(1),
    149                 8,
    150                 ServiceSqliteConnectionOptionsError::BusyTimeoutNotMilliseconds,
    151             ),
    152             (
    153                 Duration::from_micros(1_500),
    154                 8,
    155                 ServiceSqliteConnectionOptionsError::BusyTimeoutNotMilliseconds,
    156             ),
    157             (
    158                 Duration::from_millis(60_001),
    159                 8,
    160                 ServiceSqliteConnectionOptionsError::BusyTimeoutTooLarge,
    161             ),
    162             (
    163                 Duration::MAX,
    164                 8,
    165                 ServiceSqliteConnectionOptionsError::BusyTimeoutNotMilliseconds,
    166             ),
    167             (
    168                 Duration::from_secs(5),
    169                 0,
    170                 ServiceSqliteConnectionOptionsError::PoolTooSmall,
    171             ),
    172             (
    173                 Duration::from_secs(5),
    174                 9,
    175                 ServiceSqliteConnectionOptionsError::PoolTooLarge,
    176             ),
    177             (
    178                 Duration::from_secs(5),
    179                 u32::MAX,
    180                 ServiceSqliteConnectionOptionsError::PoolTooLarge,
    181             ),
    182         ];
    183 
    184         for (timeout, max_connections, expected) in vectors {
    185             let error = ServiceSqliteConnectionOptions::new(timeout, max_connections)
    186                 .expect_err("invalid options must fail");
    187             assert_eq!(error, expected);
    188             assert!(!error.to_string().contains(&max_connections.to_string()));
    189         }
    190     }
    191 }