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
37 changes: 37 additions & 0 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::{LazyLock, Mutex};
use std::time::Duration;

use deadpool_postgres::{Config, ManagerConfig, Pool, RecyclingMethod, Runtime, SslMode};
use tokio_postgres::types::{ToSql, Type};
Expand Down Expand Up @@ -426,6 +427,12 @@ fn startup_script_error(err: tokio_postgres::Error) -> String {
format!("Startup script failed: {}", format_pg_error(&err))
}

/// The startup-script preflight opens a real network connection, so bound
/// it: a broken script or a stalled host must never wedge pool creation
/// indefinitely. Matches the builtin driver's
/// `POSTGRES_STARTUP_SCRIPT_TIMEOUT_MS` (`src-tauri/src/pool_manager.rs`).
const STARTUP_SCRIPT_TIMEOUT_MS: u64 = 30_000;

/// Build the `post_create` hook that runs the startup script on every new
/// pooled connection (matches the builtin driver's `post_create` hook — see
/// `src-tauri/src/pool_manager.rs`).
Expand All @@ -449,6 +456,11 @@ fn startup_script_hook(script: &str) -> deadpool_postgres::Hook {
/// preflight exists only for early, well-labelled failures — the per-pool
/// `post_create` hook is the single place the script actually takes effect.
/// Matches the builtin driver's `run_postgres_startup_script` preflight.
///
/// Bounded by [`STARTUP_SCRIPT_TIMEOUT_MS`]: this opens a real network
/// connection and runs caller-supplied SQL, so a broken script or a stalled
/// host must never wedge pool creation (and therefore every RPC that needs
/// a pool) indefinitely.
async fn preflight_startup_script<T>(cfg: &Config, tls: T, script: &str) -> Result<(), String>
where
T: tokio_postgres::tls::MakeTlsConnect<tokio_postgres::Socket> + Clone + Sync + Send + 'static,
Expand All @@ -459,6 +471,31 @@ where
let pg_config = cfg
.get_pg_config()
.map_err(|e| format!("Pool creation failed: {e}"))?;
let timeout = Duration::from_millis(STARTUP_SCRIPT_TIMEOUT_MS);
tokio::time::timeout(
timeout,
run_startup_script_preflight(pg_config, tls, script),
)
.await
.map_err(|_| {
format!(
"Timed out running PostgreSQL startup script after {} ms",
timeout.as_millis()
)
})?
}

async fn run_startup_script_preflight<T>(
pg_config: tokio_postgres::Config,
tls: T,
script: &str,
) -> Result<(), String>
where
T: tokio_postgres::tls::MakeTlsConnect<tokio_postgres::Socket> + Clone + Sync + Send + 'static,
T::Stream: Sync + Send,
T::TlsConnect: Sync + Send,
<T::TlsConnect as tokio_postgres::tls::TlsConnect<tokio_postgres::Socket>>::Future: Send,
{
let (mut client, connection) = pg_config
.connect(tls)
.await
Expand Down
35 changes: 35 additions & 0 deletions tests/live_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,41 @@ fn broken_startup_script_fails_fast_with_clear_attribution() {
);
}

// Coverage for #85: preflight_startup_script ran the caller-supplied script
// with no timeout, so a script that blocks (or a stalled host) wedged pool
// creation — and therefore every RPC that needs a pool — indefinitely.
// pg_sleep(35) outlasts the 30-second timeout, so this must fail with a
// clear "Timed out" message well before 35s, not hang until the script
// itself finishes.
#[test]
fn hung_startup_script_times_out_instead_of_hanging_pool_creation() {
let mut plugin = Plugin::spawn();
let mut params = conn_params();
// Use a startup_script unique to this test so the pool cache key (which
// folds in startup_script) can't reuse a pool already validated by
// another test — a fresh identity guarantees the preflight actually runs.
params["startup_script"] = json!("SELECT pg_sleep(35)");

let started = std::time::Instant::now();
let response = plugin.call("test_connection", json!({ "params": params }));
let elapsed = started.elapsed();

let error = response
.get("error")
.and_then(|e| e.get("message"))
.and_then(Value::as_str)
.expect("a hung startup script must produce a JSON-RPC error, not hang forever");
assert_eq!(
error, "Timed out running PostgreSQL startup script after 30000 ms",
"error should clearly attribute the failure to the startup-script timeout"
);
assert!(
elapsed < std::time::Duration::from_secs(33),
"preflight should fail at the 30s timeout, not wait for the 35s pg_sleep to finish \
(took {elapsed:?})"
);
}

// Coverage for #43: build_pool never called cfg.ssl_mode(...), so
// tokio_postgres's own default (SslMode::Prefer) applied regardless of the
// plugin's ssl_mode value, letting ssl_mode=require silently connect over
Expand Down
Loading