diff --git a/backend/src/db.rs b/backend/src/db.rs index dab6466..01ce814 100644 --- a/backend/src/db.rs +++ b/backend/src/db.rs @@ -106,10 +106,27 @@ pub async fn create_pool(database_url: &str) -> Result { } }; + // Migrations run on their OWN connection, deliberately NOT from the pool. + // + // `after_connect` above puts `lock_timeout = 5s` on every pooled connection, and the migrator + // would inherit it. Migrations that take ACCESS EXCLUSIVE (026's index swap, 027's ADD COLUMN) + // then turn a short WAIT into a hard FAILURE: anything holding ACCESS SHARE on `upload` or + // `"user"` for more than five seconds — the hourly `pg_dump` the runbook installs in §10.2, or + // an operator's open `psql` transaction — aborts the migration, `create_pool` returns an + // error, `main` exits 1, and `restart: unless-stopped` crash-loops the app behind a live Caddy. + // The rollback is clean and a later retry succeeds, which is exactly what makes it a confusing + // intermittent outage rather than an obvious one. + // + // `statement_timeout` is left off here too: a migration on a real table can legitimately run + // longer than the 15s a request is allowed. + let mut migrator_conn = ::connect(database_url) + .await + .context("failed to open a connection for migrations")?; sqlx::migrate!() - .run(&pool) + .run(&mut migrator_conn) .await .context("failed to run database migrations")?; + let _ = sqlx::Connection::close(migrator_conn).await; tracing::info!(max_connections, "database connected and migrations applied"); Ok(pool)