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 }