1use std::time::Duration;
2
3use async_trait::async_trait;
4use sbol_db_core::DomainError;
5use sbol_db_storage::{MigrationEntry, Migrator};
6
7pub use sqlx::PgPool;
8
9static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("./migrations");
10
11#[derive(Clone)]
13pub struct PgMigrator {
14 pool: PgPool,
15}
16
17impl PgMigrator {
18 pub fn new(pool: PgPool) -> Self {
19 Self { pool }
20 }
21}
22
23#[async_trait]
24impl Migrator for PgMigrator {
25 async fn run_migrations(&self) -> Result<(), DomainError> {
26 run_migrations(&self.pool).await
27 }
28
29 async fn migration_status(&self) -> Result<Vec<MigrationEntry>, DomainError> {
30 migration_status(&self.pool).await
31 }
32}
33
34fn db_err<E: std::fmt::Display>(e: E) -> DomainError {
35 DomainError::Database(e.to_string())
36}
37
38#[derive(Debug, Clone)]
43pub struct PoolConfig {
44 pub max_connections: u32,
45 pub min_connections: u32,
46 pub acquire_timeout: Duration,
47 pub idle_timeout: Option<Duration>,
48 pub max_lifetime: Option<Duration>,
49 pub connect_timeout: Duration,
50}
51
52impl Default for PoolConfig {
53 fn default() -> Self {
54 Self {
55 max_connections: 8,
56 min_connections: 0,
57 acquire_timeout: Duration::from_secs(5),
58 idle_timeout: Some(Duration::from_secs(300)),
59 max_lifetime: Some(Duration::from_secs(1800)),
60 connect_timeout: Duration::from_secs(5),
61 }
62 }
63}
64
65impl PoolConfig {
66 pub fn from_env() -> Self {
71 let defaults = Self::default();
72 Self {
73 max_connections: env_u32("DATABASE_MAX_CONNECTIONS", defaults.max_connections),
74 min_connections: env_u32("DATABASE_MIN_CONNECTIONS", defaults.min_connections),
75 acquire_timeout: env_duration_secs(
76 "DATABASE_ACQUIRE_TIMEOUT_SECS",
77 defaults.acquire_timeout,
78 ),
79 idle_timeout: env_opt_duration_secs(
80 "DATABASE_IDLE_TIMEOUT_SECS",
81 defaults.idle_timeout,
82 ),
83 max_lifetime: env_opt_duration_secs(
84 "DATABASE_MAX_LIFETIME_SECS",
85 defaults.max_lifetime,
86 ),
87 connect_timeout: env_duration_secs(
88 "DATABASE_CONNECT_TIMEOUT_SECS",
89 defaults.connect_timeout,
90 ),
91 }
92 }
93}
94
95pub async fn connect(database_url: &str) -> Result<PgPool, DomainError> {
98 connect_with_config(database_url, &PoolConfig::from_env()).await
99}
100
101pub async fn connect_with_config(
102 database_url: &str,
103 cfg: &PoolConfig,
104) -> Result<PgPool, DomainError> {
105 let mut opts = sqlx::postgres::PgPoolOptions::new()
106 .max_connections(cfg.max_connections)
107 .min_connections(cfg.min_connections)
108 .acquire_timeout(cfg.acquire_timeout);
109 if let Some(t) = cfg.idle_timeout {
110 opts = opts.idle_timeout(t);
111 }
112 if let Some(t) = cfg.max_lifetime {
113 opts = opts.max_lifetime(t);
114 }
115 tokio::time::timeout(cfg.connect_timeout, opts.connect(database_url))
116 .await
117 .map_err(|_| DomainError::Database("connect timed out".into()))?
118 .map_err(db_err)
119}
120
121pub async fn connect_with_retry(
128 database_url: &str,
129 total_deadline: Duration,
130) -> Result<PgPool, DomainError> {
131 let cfg = PoolConfig::from_env();
132 let started = std::time::Instant::now();
133 let mut backoff = Duration::from_secs(1);
134 let max_backoff = Duration::from_secs(10);
135 loop {
136 match connect_with_config(database_url, &cfg).await {
137 Ok(pool) => return Ok(pool),
138 Err(err) => {
139 let remaining = total_deadline.saturating_sub(started.elapsed());
140 if remaining.is_zero() {
141 return Err(err);
142 }
143 let sleep_for = backoff.min(remaining);
144 tracing::warn!(
145 error = %err,
146 retry_in_secs = sleep_for.as_secs_f64(),
147 "database connect failed; retrying",
148 );
149 tokio::time::sleep(sleep_for).await;
150 backoff = (backoff * 2).min(max_backoff);
151 }
152 }
153 }
154}
155
156pub async fn run_migrations(pool: &PgPool) -> Result<(), DomainError> {
157 MIGRATOR.run(pool).await.map_err(db_err)
158}
159
160pub async fn migration_status(pool: &PgPool) -> Result<Vec<MigrationEntry>, DomainError> {
161 let mut conn = pool.acquire().await.map_err(db_err)?;
162 let applied = MIGRATOR
163 .iter()
164 .map(|m| (m.version, m.description.to_string()))
165 .collect::<Vec<_>>();
166 let applied_versions: Vec<i64> =
167 sqlx::query_scalar("SELECT version FROM _sqlx_migrations ORDER BY version")
168 .fetch_all(&mut *conn)
169 .await
170 .unwrap_or_default();
171 Ok(applied
172 .into_iter()
173 .map(|(v, d)| MigrationEntry {
174 version: v,
175 description: d,
176 applied: applied_versions.contains(&v),
177 })
178 .collect())
179}
180
181fn env_u32(name: &str, default: u32) -> u32 {
182 std::env::var(name)
183 .ok()
184 .and_then(|s| s.parse().ok())
185 .unwrap_or(default)
186}
187
188fn env_duration_secs(name: &str, default: Duration) -> Duration {
189 std::env::var(name)
190 .ok()
191 .and_then(|s| s.parse::<u64>().ok())
192 .map(Duration::from_secs)
193 .unwrap_or(default)
194}
195
196fn env_opt_duration_secs(name: &str, default: Option<Duration>) -> Option<Duration> {
197 match std::env::var(name).ok().and_then(|s| s.parse::<u64>().ok()) {
198 Some(0) => None, Some(n) => Some(Duration::from_secs(n)),
200 None => default,
201 }
202}