From 6c436f406b0b87739727fc4fa635572c29912e6c Mon Sep 17 00:00:00 2001 From: Alex Rosenzweig Date: Tue, 11 Aug 2026 10:34:49 +1000 Subject: [PATCH] fix(relay): normalize Postgres timeout units Signed-off-by: Alex Rosenzweig --- crates/buzz-relay/src/config.rs | 60 +++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/crates/buzz-relay/src/config.rs b/crates/buzz-relay/src/config.rs index 6e739b015c..07ab0a17ef 100644 --- a/crates/buzz-relay/src/config.rs +++ b/crates/buzz-relay/src/config.rs @@ -342,7 +342,12 @@ fn pg_timeout_or_default(raw: Option<&str>, default: &str) -> String { let unit = unit.trim().to_ascii_lowercase(); match pg_timeout_millis(magnitude, &unit) { - Some(millis) if millis <= buzz_db::PG_TIMEOUT_MAX_MILLIS => candidate.to_string(), + // Return the spelling we validated. PostgreSQL's timeout units are + // case-sensitive, so returning `candidate` here would let `45S` pass + // validation and then fail every pool's `after_connect`. + Some(millis) if millis <= buzz_db::PG_TIMEOUT_MAX_MILLIS => { + format!("{magnitude}{unit}") + } Some(millis) => { tracing::warn!( value = candidate, @@ -1126,6 +1131,7 @@ impl Config { #[cfg(test)] mod tests { use super::*; + use sqlx::Connection; // Mutex to serialize tests that mutate environment variables. // Parallel env-var mutation causes `defaults_are_valid` to see the invalid @@ -1344,10 +1350,17 @@ mod tests { #[test] fn pg_timeout_accepts_postgres_spellings_and_refuses_the_rest() { - for accepted in ["30s", "500ms", "0", "45S", "2min", " 10s "] { + for (accepted, canonical) in [ + ("30s", "30s"), + ("500ms", "500ms"), + ("0", "0"), + ("45S", "45s"), + ("2MIN", "2min"), + (" 10 H ", "10h"), + ] { assert_eq!( pg_timeout_or_default(Some(accepted), "30s"), - accepted.trim(), + canonical, "{accepted} is a valid Postgres timeout" ); } @@ -1365,6 +1378,41 @@ mod tests { assert_eq!(pg_timeout_or_default(None, "5s"), "5s"); } + /// Every supported unit spelling emitted by the parser must be usable by + /// PostgreSQL. A parser-only assertion missed that PostgreSQL rejects + /// uppercase units even though validation lowercases them. + #[tokio::test] + #[ignore = "requires Postgres"] + async fn pg_timeout_canonical_spellings_are_accepted_by_postgres() { + let database_url = std::env::var("BUZZ_TEST_DATABASE_URL") + .or_else(|_| std::env::var("DATABASE_URL")) + .unwrap_or_else(|_| { + "postgres://buzz:buzz_dev@localhost:5432/buzz".to_string() // sadscan:disable np.postgres.1 + }); + let mut connection = sqlx::PgConnection::connect(&database_url) + .await + .expect("connect test Postgres"); + + for (raw, canonical) in [ + ("500US", "500us"), + ("500MS", "500ms"), + ("2S", "2s"), + ("2MIN", "2min"), + ("2H", "2h"), + ("2D", "2d"), + ("0", "0"), + (" 10 S ", "10s"), + ] { + let parsed = pg_timeout_or_default(Some(raw), "30s"); + assert_eq!(parsed, canonical); + buzz_db::apply_runtime_connection_timeouts(&mut connection, &parsed, &parsed) + .await + .unwrap_or_else(|error| panic!("{raw:?} normalized to {parsed:?}: {error}")); + } + + connection.close().await.expect("close test Postgres"); + } + #[test] fn pg_timeout_refuses_magnitudes_postgres_cannot_store() { // Well-formed spellings whose millisecond value exceeds the int GUC @@ -1432,6 +1480,12 @@ mod tests { assert_eq!(overridden.db_statement_timeout, "90s"); assert_eq!(overridden.db_lock_timeout, "250ms"); + std::env::set_var("BUZZ_DB_STATEMENT_TIMEOUT", "45S"); + std::env::set_var("BUZZ_DB_LOCK_TIMEOUT", " 2 MIN "); + let canonicalized = Config::from_env().expect("config"); + assert_eq!(canonicalized.db_statement_timeout, "45s"); + assert_eq!(canonicalized.db_lock_timeout, "2min"); + std::env::set_var("BUZZ_DB_STATEMENT_TIMEOUT", "half a minute"); let junk = Config::from_env().expect("config"); assert_eq!(