diff --git a/configuration.md b/configuration.md index 77e6b322..c5911799 100644 --- a/configuration.md +++ b/configuration.md @@ -15,8 +15,8 @@ Here are the available configuration options and their default values: | `unix_socket` | | Path to a UNIX socket to listen on instead of the TCP port. If specified, SQLPage will accept HTTP connections only on this socket and not on any TCP port. This option is mutually exclusive with `listen_on` and `port`. | `host` | | The web address where your application is accessible (e.g., "myapp.example.com"). Used for login redirects with OIDC. | | `max_database_pool_connections` | PostgreSQL: 50
MySql: 75
SQLite: 16
MSSQL: 100 | How many simultaneous database connections to open at most | -| `database_connection_idle_timeout_seconds` | SQLite: None
All other: 30 minutes | Automatically close database connections after this period of inactivity | -| `database_connection_max_lifetime_seconds` | SQLite: None
All other: 60 minutes | Always close database connections after this amount of time | +| `database_connection_idle_timeout_seconds` | SQLite: None
All other: 30 minutes | Automatically close database connections after this period of inactivity. Set to 0 to disable. | +| `database_connection_max_lifetime_seconds` | SQLite: None
All other: 60 minutes | Always close database connections after this amount of time. Set to 0 to disable. | | `database_connection_retries` | 6 | Database connection attempts before giving up. Retries will happen every 5 seconds. | | `database_connection_acquire_timeout_seconds` | 10 | How long to wait when acquiring a database connection from the pool before giving up and returning an error. | | `sqlite_extensions` | | An array of SQLite extensions to load, such as `mod_spatialite` | diff --git a/src/app_config.rs b/src/app_config.rs index 5ebcab63..9e601ade 100644 --- a/src/app_config.rs +++ b/src/app_config.rs @@ -9,6 +9,7 @@ use serde::de::Error; use serde::{Deserialize, Deserializer, Serialize}; use std::net::{SocketAddr, ToSocketAddrs}; use std::path::{Path, PathBuf}; +use std::time::Duration; #[cfg(not(feature = "lambda-web"))] const DEFAULT_DATABASE_FILE: &str = "sqlpage.db"; @@ -73,6 +74,8 @@ impl AppConfig { .validate() .context("The provided configuration is invalid")?; + config.resolve_timeouts(); + log::debug!("Loaded configuration: {config:#?}"); log::info!( "Configuration loaded from {}", @@ -82,6 +85,26 @@ impl AppConfig { Ok(config) } + fn resolve_timeouts(&mut self) { + let is_sqlite = self.database_url.starts_with("sqlite:"); + self.database_connection_idle_timeout = resolve_timeout( + self.database_connection_idle_timeout, + if is_sqlite { + None + } else { + Some(Duration::from_secs(30 * 60)) + }, + ); + self.database_connection_max_lifetime = resolve_timeout( + self.database_connection_max_lifetime, + if is_sqlite { + None + } else { + Some(Duration::from_secs(60 * 60)) + }, + ); + } + fn validate(&self) -> anyhow::Result<()> { if !self.web_root.is_dir() { return Err(anyhow::anyhow!( @@ -107,20 +130,6 @@ impl AppConfig { )); } } - if let Some(idle_timeout) = self.database_connection_idle_timeout_seconds { - if idle_timeout < 0.0 { - return Err(anyhow::anyhow!( - "Database connection idle timeout must be non-negative" - )); - } - } - if let Some(max_lifetime) = self.database_connection_max_lifetime_seconds { - if max_lifetime < 0.0 { - return Err(anyhow::anyhow!( - "Database connection max lifetime must be non-negative" - )); - } - } anyhow::ensure!(self.max_pending_rows > 0, "max_pending_rows cannot be null"); Ok(()) } @@ -146,8 +155,18 @@ pub struct AppConfig { #[serde(default)] pub database_password: Option, pub max_database_pool_connections: Option, - pub database_connection_idle_timeout_seconds: Option, - pub database_connection_max_lifetime_seconds: Option, + #[serde( + default, + deserialize_with = "deserialize_duration_seconds", + rename = "database_connection_idle_timeout_seconds" + )] + pub database_connection_idle_timeout: Option, + #[serde( + default, + deserialize_with = "deserialize_duration_seconds", + rename = "database_connection_max_lifetime_seconds" + )] + pub database_connection_max_lifetime: Option, #[serde(default)] pub sqlite_extensions: Vec, @@ -611,6 +630,26 @@ impl DevOrProd { } } +fn deserialize_duration_seconds<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let seconds: Option = Option::deserialize(deserializer)?; + match seconds { + None => Ok(None), + Some(s) if s <= 0.0 || !s.is_finite() => Ok(Some(Duration::ZERO)), + Some(s) => Ok(Some(Duration::from_secs_f64(s))), + } +} + +fn resolve_timeout(config_val: Option, default: Option) -> Option { + match config_val { + Some(v) if v.is_zero() => None, + Some(v) => Some(v), + None => default, + } +} + #[must_use] pub fn test_database_url() -> String { std::env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite::memory:".to_string()) @@ -623,14 +662,16 @@ pub mod tests { #[must_use] pub fn test_config() -> AppConfig { - serde_json::from_str::( + let mut config = serde_json::from_str::( &serde_json::json!({ "database_url": test_database_url(), "listen_on": "localhost:8080" }) .to_string(), ) - .unwrap() + .unwrap(); + config.resolve_timeouts(); + config } } diff --git a/src/webserver/database/connect.rs b/src/webserver/database/connect.rs index 4ece367a..d96f2b5a 100644 --- a/src/webserver/database/connect.rs +++ b/src/webserver/database/connect.rs @@ -89,24 +89,8 @@ impl Database { AnyKind::Mssql => 100, } }) - .idle_timeout( - config - .database_connection_idle_timeout_seconds - .map(Duration::from_secs_f64) - .or_else(|| match kind { - AnyKind::Sqlite => None, - _ => Some(Duration::from_secs(30 * 60)), - }), - ) - .max_lifetime( - config - .database_connection_max_lifetime_seconds - .map(Duration::from_secs_f64) - .or_else(|| match kind { - AnyKind::Sqlite => None, - _ => Some(Duration::from_secs(60 * 60)), - }), - ) + .idle_timeout(config.database_connection_idle_timeout) + .max_lifetime(config.database_connection_max_lifetime) .acquire_timeout(Duration::from_secs_f64( config.database_connection_acquire_timeout_seconds, ));