Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<BR> MySql: 75<BR> SQLite: 16<BR> MSSQL: 100 | How many simultaneous database connections to open at most |
| `database_connection_idle_timeout_seconds` | SQLite: None<BR> All other: 30 minutes | Automatically close database connections after this period of inactivity |
| `database_connection_max_lifetime_seconds` | SQLite: None<BR> All other: 60 minutes | Always close database connections after this amount of time |
| `database_connection_idle_timeout_seconds` | SQLite: None<BR> 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<BR> 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` |
Expand Down
77 changes: 59 additions & 18 deletions src/app_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 {}",
Expand All @@ -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!(
Expand All @@ -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(())
}
Expand All @@ -146,8 +155,18 @@ pub struct AppConfig {
#[serde(default)]
pub database_password: Option<String>,
pub max_database_pool_connections: Option<u32>,
pub database_connection_idle_timeout_seconds: Option<f64>,
pub database_connection_max_lifetime_seconds: Option<f64>,
#[serde(
default,
deserialize_with = "deserialize_duration_seconds",
rename = "database_connection_idle_timeout_seconds"
)]
pub database_connection_idle_timeout: Option<Duration>,
#[serde(
default,
deserialize_with = "deserialize_duration_seconds",
rename = "database_connection_max_lifetime_seconds"
)]
pub database_connection_max_lifetime: Option<Duration>,

#[serde(default)]
pub sqlite_extensions: Vec<String>,
Expand Down Expand Up @@ -611,6 +630,26 @@ impl DevOrProd {
}
}

fn deserialize_duration_seconds<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
where
D: Deserializer<'de>,
{
let seconds: Option<f64> = 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<Duration>, default: Option<Duration>) -> Option<Duration> {
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())
Expand All @@ -623,14 +662,16 @@ pub mod tests {

#[must_use]
pub fn test_config() -> AppConfig {
serde_json::from_str::<AppConfig>(
let mut config = serde_json::from_str::<AppConfig>(
&serde_json::json!({
"database_url": test_database_url(),
"listen_on": "localhost:8080"
})
.to_string(),
)
.unwrap()
.unwrap();
config.resolve_timeouts();
config
}
}

Expand Down
20 changes: 2 additions & 18 deletions src/webserver/database/connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
));
Expand Down