fix: size the DB pool and fail fast on saturation
The single Postgres pool (shared by HTTP handlers and the crawler/analysis daemons) hardcoded max_connections=10 with no acquire_timeout, so under saturation callers hung on the driver's silent 30s default. Add a DbConfig (DB_MAX_CONNECTIONS default 20, DB_ACQUIRE_TIMEOUT_SECS default 10, clamped to >=1) and apply both in PgPoolOptions. Bump to 0.124.4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2
backend/Cargo.lock
generated
2
backend/Cargo.lock
generated
@@ -1558,7 +1558,7 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||
|
||||
[[package]]
|
||||
name = "mangalord"
|
||||
version = "0.124.3"
|
||||
version = "0.124.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mangalord"
|
||||
version = "0.124.3"
|
||||
version = "0.124.4"
|
||||
edition = "2021"
|
||||
default-run = "mangalord"
|
||||
|
||||
|
||||
@@ -279,7 +279,8 @@ impl DaemonReloader for Supervisors {
|
||||
|
||||
pub async fn build(config: Config) -> anyhow::Result<AppHandle> {
|
||||
let db = PgPoolOptions::new()
|
||||
.max_connections(10)
|
||||
.max_connections(config.db.max_connections)
|
||||
.acquire_timeout(config.db.acquire_timeout)
|
||||
.connect(&config.database_url)
|
||||
.await?;
|
||||
sqlx::migrate!("./migrations").run(&db).await?;
|
||||
|
||||
@@ -72,6 +72,45 @@ impl Default for UploadConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Postgres connection-pool sizing. One pool backs every HTTP handler plus
|
||||
/// the crawler/analysis daemons, so it must be large enough not to starve
|
||||
/// interactive reads and fail fast (rather than hang on the driver's silent
|
||||
/// 30 s default) when genuinely saturated.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DbConfig {
|
||||
/// `DB_MAX_CONNECTIONS`. Upper bound on open connections.
|
||||
pub max_connections: u32,
|
||||
/// `DB_ACQUIRE_TIMEOUT_SECS`. How long a caller waits for a free
|
||||
/// connection before erroring — short so overload surfaces as a fast
|
||||
/// 500 instead of a 30 s hang.
|
||||
pub acquire_timeout: Duration,
|
||||
}
|
||||
|
||||
impl Default for DbConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_connections: 20,
|
||||
acquire_timeout: Duration::from_secs(10),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DbConfig {
|
||||
pub fn from_env() -> Self {
|
||||
let default = Self::default();
|
||||
Self {
|
||||
// `.max(1)`: a zero-size pool can never hand out a connection and
|
||||
// would deadlock every query — clamp to at least one.
|
||||
max_connections: env_u64("DB_MAX_CONNECTIONS", default.max_connections.into())
|
||||
.max(1) as u32,
|
||||
acquire_timeout: Duration::from_secs(env_u64(
|
||||
"DB_ACQUIRE_TIMEOUT_SECS",
|
||||
default.acquire_timeout.as_secs(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How the worker asks the model to constrain its output. OpenAI-compatible
|
||||
/// servers disagree here: LM Studio accepts only `json_schema` or `text`
|
||||
/// (NOT `json_object`); vanilla OpenAI/vLLM accept `json_object` too. The
|
||||
@@ -371,6 +410,7 @@ pub struct Config {
|
||||
pub database_url: String,
|
||||
pub bind_address: String,
|
||||
pub storage_dir: PathBuf,
|
||||
pub db: DbConfig,
|
||||
pub auth: AuthConfig,
|
||||
pub upload: UploadConfig,
|
||||
pub cors_allowed_origins: Vec<String>,
|
||||
@@ -517,6 +557,7 @@ impl Config {
|
||||
storage_dir: std::env::var("STORAGE_DIR")
|
||||
.unwrap_or_else(|_| "./data/storage".to_string())
|
||||
.into(),
|
||||
db: DbConfig::from_env(),
|
||||
auth: AuthConfig {
|
||||
cookie_secure: env_bool("COOKIE_SECURE", true),
|
||||
cookie_domain: std::env::var("COOKIE_DOMAIN")
|
||||
@@ -813,6 +854,37 @@ mod tests {
|
||||
assert_eq!(cfg.browser_restart_threshold, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn db_pool_defaults_when_unset() {
|
||||
let _g = ENV_GUARD.lock().unwrap_or_else(|p| p.into_inner());
|
||||
std::env::remove_var("DB_MAX_CONNECTIONS");
|
||||
std::env::remove_var("DB_ACQUIRE_TIMEOUT_SECS");
|
||||
let cfg = DbConfig::from_env();
|
||||
assert_eq!(cfg.max_connections, 20);
|
||||
assert_eq!(cfg.acquire_timeout, Duration::from_secs(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn db_pool_parses_from_env() {
|
||||
let _g = ENV_GUARD.lock().unwrap_or_else(|p| p.into_inner());
|
||||
std::env::set_var("DB_MAX_CONNECTIONS", "50");
|
||||
std::env::set_var("DB_ACQUIRE_TIMEOUT_SECS", "3");
|
||||
let cfg = DbConfig::from_env();
|
||||
std::env::remove_var("DB_MAX_CONNECTIONS");
|
||||
std::env::remove_var("DB_ACQUIRE_TIMEOUT_SECS");
|
||||
assert_eq!(cfg.max_connections, 50);
|
||||
assert_eq!(cfg.acquire_timeout, Duration::from_secs(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn db_pool_max_connections_clamps_to_at_least_one() {
|
||||
let _g = ENV_GUARD.lock().unwrap_or_else(|p| p.into_inner());
|
||||
std::env::set_var("DB_MAX_CONNECTIONS", "0");
|
||||
let cfg = DbConfig::from_env();
|
||||
std::env::remove_var("DB_MAX_CONNECTIONS");
|
||||
assert_eq!(cfg.max_connections, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analysis_config_defaults_when_unset() {
|
||||
let _g = ENV_GUARD.lock().unwrap_or_else(|p| p.into_inner());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mangalord-frontend",
|
||||
"version": "0.124.3",
|
||||
"version": "0.124.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user