tinycongress_api/
db.rs

1use crate::config::DatabaseConfig;
2use sqlx_core::migrate::Migrator;
3use sqlx_postgres::{PgPool, PgPoolOptions};
4use std::path::{Path, PathBuf};
5use std::time::{Duration, Instant};
6use tokio::time::sleep;
7use tracing::{info, warn};
8
9/// Connect to the database and run migrations.
10///
11/// This function implements exponential backoff retry logic to handle
12/// startup race conditions when the database container is still initializing.
13///
14/// # Errors
15/// Returns an error if the database connection cannot be established or
16/// migrations fail to run after exhausting retries.
17pub async fn setup_database(config: &DatabaseConfig) -> Result<PgPool, anyhow::Error> {
18    let retry_deadline = Duration::from_secs(60); // overall retry budget
19    let max_interval = Duration::from_secs(30); // cap single waits
20    let mut delay = Duration::from_millis(500);
21    let start = Instant::now();
22
23    let pool = loop {
24        info!("Attempting to connect to Postgres...");
25
26        match PgPoolOptions::new()
27            .max_connections(config.max_connections)
28            // Allow extra time to acquire a connection during startup bursts
29            .acquire_timeout(Duration::from_secs(30))
30            .connect(&config.url)
31            .await
32        {
33            Ok(pool) => break pool,
34            Err(err) => {
35                if start.elapsed() >= retry_deadline {
36                    warn!(error = %err, "Postgres not ready; retries exhausted");
37                    return Err(err.into());
38                }
39
40                warn!(error = %err, "Postgres not ready yet; retrying");
41                sleep(delay).await;
42                delay = (delay.saturating_mul(2)).min(max_interval);
43            }
44        }
45    };
46
47    // Resolve the migrations directory in a way that works in release images too.
48    // Preference order:
49    //  1. config.migrations_dir (from config file or TC_DATABASE__MIGRATIONS_DIR env)
50    //  2. ./migrations relative to the running binary
51    //  3. The compile-time manifest directory for local `cargo run`
52    let candidate_dirs = [
53        config.migrations_dir.as_ref().map(PathBuf::from),
54        Some(PathBuf::from("./migrations")),
55        Some(PathBuf::from(concat!(
56            env!("CARGO_MANIFEST_DIR"),
57            "/migrations"
58        ))),
59    ];
60
61    let mut last_error = None;
62    let mut migrator = None;
63
64    for dir in candidate_dirs.into_iter().flatten() {
65        match Migrator::new(Path::new(&dir)).await {
66            Ok(found) => {
67                info!("Using migrations from {}", dir.display());
68                migrator = Some(found);
69                break;
70            }
71            Err(err) => {
72                last_error = Some((dir, err));
73            }
74        }
75    }
76
77    let migrator = migrator.ok_or_else(|| match last_error {
78        Some((dir, err)) => {
79            anyhow::anyhow!("failed to load migrations from {}: {}", dir.display(), err)
80        }
81        None => anyhow::anyhow!("failed to resolve migrations directory"),
82    })?;
83
84    migrator.run(&pool).await?;
85    info!("Migrations applied");
86    Ok(pool)
87}