Skip to content

Commit dc5da46

Browse files
lovasoacursoragent
andauthored
Connection timeouts configuration (#1197)
* Allow disabling database connection timeouts via config Co-authored-by: contact <contact@ophir.dev> * Simplify documentation for disabling database timeouts Co-authored-by: contact <contact@ophir.dev> * Refactor timeout resolution to AppConfig Co-authored-by: contact <contact@ophir.dev> * Remove *_raw fields and use custom deserializer for timeouts Co-authored-by: contact <contact@ophir.dev> * Fix formatting in AppConfig Co-authored-by: contact <contact@ophir.dev> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent cb4c1f9 commit dc5da46

File tree

3 files changed

+63
-38
lines changed

3 files changed

+63
-38
lines changed

configuration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ Here are the available configuration options and their default values:
1515
| `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`.
1616
| `host` | | The web address where your application is accessible (e.g., "myapp.example.com"). Used for login redirects with OIDC. |
1717
| `max_database_pool_connections` | PostgreSQL: 50<BR> MySql: 75<BR> SQLite: 16<BR> MSSQL: 100 | How many simultaneous database connections to open at most |
18-
| `database_connection_idle_timeout_seconds` | SQLite: None<BR> All other: 30 minutes | Automatically close database connections after this period of inactivity |
19-
| `database_connection_max_lifetime_seconds` | SQLite: None<BR> All other: 60 minutes | Always close database connections after this amount of time |
18+
| `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. |
19+
| `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. |
2020
| `database_connection_retries` | 6 | Database connection attempts before giving up. Retries will happen every 5 seconds. |
2121
| `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. |
2222
| `sqlite_extensions` | | An array of SQLite extensions to load, such as `mod_spatialite` |

src/app_config.rs

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use serde::de::Error;
99
use serde::{Deserialize, Deserializer, Serialize};
1010
use std::net::{SocketAddr, ToSocketAddrs};
1111
use std::path::{Path, PathBuf};
12+
use std::time::Duration;
1213

1314
#[cfg(not(feature = "lambda-web"))]
1415
const DEFAULT_DATABASE_FILE: &str = "sqlpage.db";
@@ -73,6 +74,8 @@ impl AppConfig {
7374
.validate()
7475
.context("The provided configuration is invalid")?;
7576

77+
config.resolve_timeouts();
78+
7679
log::debug!("Loaded configuration: {config:#?}");
7780
log::info!(
7881
"Configuration loaded from {}",
@@ -82,6 +85,26 @@ impl AppConfig {
8285
Ok(config)
8386
}
8487

88+
fn resolve_timeouts(&mut self) {
89+
let is_sqlite = self.database_url.starts_with("sqlite:");
90+
self.database_connection_idle_timeout = resolve_timeout(
91+
self.database_connection_idle_timeout,
92+
if is_sqlite {
93+
None
94+
} else {
95+
Some(Duration::from_secs(30 * 60))
96+
},
97+
);
98+
self.database_connection_max_lifetime = resolve_timeout(
99+
self.database_connection_max_lifetime,
100+
if is_sqlite {
101+
None
102+
} else {
103+
Some(Duration::from_secs(60 * 60))
104+
},
105+
);
106+
}
107+
85108
fn validate(&self) -> anyhow::Result<()> {
86109
if !self.web_root.is_dir() {
87110
return Err(anyhow::anyhow!(
@@ -107,20 +130,6 @@ impl AppConfig {
107130
));
108131
}
109132
}
110-
if let Some(idle_timeout) = self.database_connection_idle_timeout_seconds {
111-
if idle_timeout < 0.0 {
112-
return Err(anyhow::anyhow!(
113-
"Database connection idle timeout must be non-negative"
114-
));
115-
}
116-
}
117-
if let Some(max_lifetime) = self.database_connection_max_lifetime_seconds {
118-
if max_lifetime < 0.0 {
119-
return Err(anyhow::anyhow!(
120-
"Database connection max lifetime must be non-negative"
121-
));
122-
}
123-
}
124133
anyhow::ensure!(self.max_pending_rows > 0, "max_pending_rows cannot be null");
125134
Ok(())
126135
}
@@ -146,8 +155,18 @@ pub struct AppConfig {
146155
#[serde(default)]
147156
pub database_password: Option<String>,
148157
pub max_database_pool_connections: Option<u32>,
149-
pub database_connection_idle_timeout_seconds: Option<f64>,
150-
pub database_connection_max_lifetime_seconds: Option<f64>,
158+
#[serde(
159+
default,
160+
deserialize_with = "deserialize_duration_seconds",
161+
rename = "database_connection_idle_timeout_seconds"
162+
)]
163+
pub database_connection_idle_timeout: Option<Duration>,
164+
#[serde(
165+
default,
166+
deserialize_with = "deserialize_duration_seconds",
167+
rename = "database_connection_max_lifetime_seconds"
168+
)]
169+
pub database_connection_max_lifetime: Option<Duration>,
151170

152171
#[serde(default)]
153172
pub sqlite_extensions: Vec<String>,
@@ -611,6 +630,26 @@ impl DevOrProd {
611630
}
612631
}
613632

633+
fn deserialize_duration_seconds<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
634+
where
635+
D: Deserializer<'de>,
636+
{
637+
let seconds: Option<f64> = Option::deserialize(deserializer)?;
638+
match seconds {
639+
None => Ok(None),
640+
Some(s) if s <= 0.0 || !s.is_finite() => Ok(Some(Duration::ZERO)),
641+
Some(s) => Ok(Some(Duration::from_secs_f64(s))),
642+
}
643+
}
644+
645+
fn resolve_timeout(config_val: Option<Duration>, default: Option<Duration>) -> Option<Duration> {
646+
match config_val {
647+
Some(v) if v.is_zero() => None,
648+
Some(v) => Some(v),
649+
None => default,
650+
}
651+
}
652+
614653
#[must_use]
615654
pub fn test_database_url() -> String {
616655
std::env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite::memory:".to_string())
@@ -623,14 +662,16 @@ pub mod tests {
623662

624663
#[must_use]
625664
pub fn test_config() -> AppConfig {
626-
serde_json::from_str::<AppConfig>(
665+
let mut config = serde_json::from_str::<AppConfig>(
627666
&serde_json::json!({
628667
"database_url": test_database_url(),
629668
"listen_on": "localhost:8080"
630669
})
631670
.to_string(),
632671
)
633-
.unwrap()
672+
.unwrap();
673+
config.resolve_timeouts();
674+
config
634675
}
635676
}
636677

src/webserver/database/connect.rs

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -89,24 +89,8 @@ impl Database {
8989
AnyKind::Mssql => 100,
9090
}
9191
})
92-
.idle_timeout(
93-
config
94-
.database_connection_idle_timeout_seconds
95-
.map(Duration::from_secs_f64)
96-
.or_else(|| match kind {
97-
AnyKind::Sqlite => None,
98-
_ => Some(Duration::from_secs(30 * 60)),
99-
}),
100-
)
101-
.max_lifetime(
102-
config
103-
.database_connection_max_lifetime_seconds
104-
.map(Duration::from_secs_f64)
105-
.or_else(|| match kind {
106-
AnyKind::Sqlite => None,
107-
_ => Some(Duration::from_secs(60 * 60)),
108-
}),
109-
)
92+
.idle_timeout(config.database_connection_idle_timeout)
93+
.max_lifetime(config.database_connection_max_lifetime)
11094
.acquire_timeout(Duration::from_secs_f64(
11195
config.database_connection_acquire_timeout_seconds,
11296
));

0 commit comments

Comments
 (0)