cratefield_adapter_postgres/
registry.rs1use std::collections::HashMap;
24use std::sync::Arc;
25use std::time::Duration;
26
27use async_trait::async_trait;
28use cratefield_core::{
29 Clock, Database, DbError, Rows, Statement, Tenant, TenantDatabases, TenantDbError,
30};
31
32use crate::{PoolLimits, Postgres};
33
34#[async_trait]
47pub trait TenantDsns: Send + Sync {
48 async fn dsn(&self, tenant: &str) -> Result<Option<String>, String>;
54}
55
56pub const TENANT_POOL_TOTAL: usize = 64;
65
66pub const TENANT_POOL_IDLE: Duration = Duration::from_secs(300);
68
69struct Cached {
70 db: Arc<CappedDb>,
71 last_used: i64,
74}
75
76pub struct PoolRegistry {
78 dsns: Arc<dyn TenantDsns>,
79 limits: PoolLimits,
80 idle: Duration,
81 clock: Arc<dyn Clock>,
82 total: Arc<tokio::sync::Semaphore>,
83 #[allow(clippy::disallowed_types)]
86 pools: std::sync::Mutex<HashMap<String, Cached>>,
87}
88
89impl PoolRegistry {
90 #[must_use]
92 pub fn new(dsns: Arc<dyn TenantDsns>, clock: Arc<dyn Clock>) -> Self {
93 Self::with_limits(
94 dsns,
95 clock,
96 PoolLimits::default(),
97 TENANT_POOL_TOTAL,
98 TENANT_POOL_IDLE,
99 )
100 }
101
102 #[allow(clippy::disallowed_types)]
107 #[must_use]
108 pub fn with_limits(
109 dsns: Arc<dyn TenantDsns>,
110 clock: Arc<dyn Clock>,
111 limits: PoolLimits,
112 total: usize,
113 idle: Duration,
114 ) -> Self {
115 Self {
116 dsns,
117 limits,
118 idle,
119 clock,
120 total: Arc::new(tokio::sync::Semaphore::new(total.max(1))),
121 pools: std::sync::Mutex::new(HashMap::new()),
122 }
123 }
124
125 #[must_use]
136 pub fn open_pools(&self) -> usize {
137 self.pools.lock().expect("pool lock").len()
138 }
139
140 fn evict_idle(&self, now: i64) -> Vec<Arc<CappedDb>> {
144 let cutoff = now.saturating_sub(i64::try_from(self.idle.as_secs()).unwrap_or(i64::MAX));
145 let mut pools = self.pools.lock().expect("pool lock");
146 let stale: Vec<String> = pools
147 .iter()
148 .filter(|(_, cached)| cached.last_used < cutoff)
149 .map(|(tenant, _)| tenant.clone())
150 .collect();
151 stale
155 .into_iter()
156 .filter_map(|tenant| pools.remove(&tenant).map(|cached| cached.db))
157 .collect()
158 }
159
160 fn now_secs(&self) -> i64 {
161 self.clock.now().unix_timestamp()
162 }
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub(crate) enum PoolError {
170 NoDsn,
172 Lookup,
174 Connect,
176}
177
178impl PoolRegistry {
179 pub(crate) async fn pool_for(&self, key: &str) -> Result<Arc<CappedDb>, PoolError> {
189 let now = self.now_secs();
190
191 for stale in self.evict_idle(now) {
192 let _ = stale.inner.close().await;
193 }
194
195 if let Some(cached) = self.pools.lock().expect("pool lock").get_mut(key) {
196 cached.last_used = now;
197 return Ok(Arc::clone(&cached.db));
198 }
199
200 let dsn = self
203 .dsns
204 .dsn(key)
205 .await
206 .map_err(|_| PoolError::Lookup)?
207 .ok_or(PoolError::NoDsn)?;
208 let pool = Postgres::connect_with(&dsn, self.limits)
209 .await
210 .map_err(|_| PoolError::Connect)?;
211 drop(dsn);
212
213 let db = Arc::new(CappedDb {
214 inner: pool,
215 total: Arc::clone(&self.total),
216 acquire_timeout: self.limits.acquire_timeout,
217 clock: Arc::clone(&self.clock),
218 key: key.to_owned(),
219 });
220 self.pools.lock().expect("pool lock").insert(
221 key.to_owned(),
222 Cached {
223 db: Arc::clone(&db),
224 last_used: now,
225 },
226 );
227 Ok(db)
228 }
229}
230
231#[async_trait]
232impl TenantDatabases for PoolRegistry {
233 async fn database(&self, tenant: &Tenant) -> Result<Arc<dyn Database>, TenantDbError> {
242 match self.pool_for(tenant.id().as_str()).await {
243 Ok(db) => Ok(db as Arc<dyn Database>),
244 Err(why) => {
245 tracing::warn!(tenant = %tenant.id(), reason = ?why, "no pool for tenant");
246 Err(TenantDbError::Unreachable {
247 tenant: tenant.id().to_string(),
248 })
249 }
250 }
251 }
252}
253
254pub(crate) struct CappedDb {
257 inner: Postgres,
258 total: Arc<tokio::sync::Semaphore>,
259 acquire_timeout: Duration,
260 clock: Arc<dyn Clock>,
261 key: String,
262}
263
264impl CappedDb {
265 async fn permit(&self) -> Result<tokio::sync::OwnedSemaphorePermit, DbError> {
271 let total = Arc::clone(&self.total);
272 let acquire = async move { total.acquire_owned().await };
273 match cratefield_core::timeout(&*self.clock, acquire, self.acquire_timeout).await {
274 Some(Ok(permit)) => Ok(permit),
275 Some(Err(_)) | None => Err(DbError::Execute(format!(
279 "`{}` waited out the connection budget",
280 self.key
281 ))),
282 }
283 }
284}
285
286#[async_trait]
287impl Database for CappedDb {
288 async fn execute(&self, stmt: &Statement) -> Result<u64, DbError> {
289 let _permit = self.permit().await?;
290 self.inner.execute(stmt).await
291 }
292
293 async fn query(&self, stmt: &Statement) -> Result<Rows, DbError> {
294 let _permit = self.permit().await?;
295 self.inner.query(stmt).await
296 }
297
298 async fn batch_atomic(&self, stmts: &[Statement]) -> Result<(), DbError> {
299 let _permit = self.permit().await?;
303 self.inner.batch_atomic(stmts).await
304 }
305}
306
307#[cfg(test)]
308#[allow(clippy::disallowed_types)] mod tests {
310 use super::*;
311 use crate::testing::{TempDb, base_url, skip_reason};
312 use cratefield_core::Statement;
313
314 struct StepClock {
317 offset: std::sync::atomic::AtomicI64,
318 }
319
320 impl StepClock {
321 fn new() -> Arc<Self> {
322 Arc::new(Self {
323 offset: std::sync::atomic::AtomicI64::new(0),
324 })
325 }
326
327 fn advance(&self, secs: i64) {
328 self.offset
329 .fetch_add(secs, std::sync::atomic::Ordering::SeqCst);
330 }
331 }
332
333 #[async_trait]
334 impl Clock for StepClock {
335 fn now(&self) -> time::OffsetDateTime {
336 time::OffsetDateTime::now_utc()
337 + time::Duration::seconds(self.offset.load(std::sync::atomic::Ordering::SeqCst))
338 }
339
340 async fn timeout_any(
341 &self,
342 fut: cratefield_core::BoxFuture<'static, Box<dyn std::any::Any + Send>>,
343 after: Duration,
344 ) -> Option<Box<dyn std::any::Any + Send>> {
345 tokio::time::timeout(after, fut).await.ok()
346 }
347 }
348
349 struct Dsns {
352 url: String,
353 known: Vec<String>,
354 lookups: std::sync::Mutex<Vec<String>>,
355 }
356
357 impl Dsns {
358 fn new(url: &str, known: &[&str]) -> Arc<Self> {
359 Arc::new(Self {
360 url: url.to_owned(),
361 known: known.iter().map(|k| (*k).to_owned()).collect(),
362 lookups: std::sync::Mutex::new(Vec::new()),
363 })
364 }
365
366 fn lookups(&self) -> Vec<String> {
367 self.lookups.lock().expect("lookup lock").clone()
368 }
369 }
370
371 #[async_trait]
372 impl TenantDsns for Dsns {
373 async fn dsn(&self, key: &str) -> Result<Option<String>, String> {
374 self.lookups
375 .lock()
376 .expect("lookup lock")
377 .push(key.to_owned());
378 Ok(self
379 .known
380 .iter()
381 .any(|known| known == key)
382 .then(|| self.url.clone()))
383 }
384 }
385
386 #[tokio::test]
387 async fn a_pool_opens_on_first_use_and_is_reused() {
388 let Some(url) = base_url() else {
389 eprintln!("SKIPPED: {}", skip_reason());
390 return;
391 };
392 let Some(temp) = TempDb::create(&url, "reglazy").await else {
393 eprintln!("SKIPPED: {}", skip_reason());
394 return;
395 };
396 let dsns = Dsns::new(&temp.url, &["a"]);
397 let registry =
398 PoolRegistry::new(Arc::clone(&dsns) as Arc<dyn TenantDsns>, StepClock::new());
399
400 assert_eq!(registry.open_pools(), 0, "nothing opens at construction");
401 for _ in 0..3 {
402 let db = registry.pool_for("a").await.expect("opens");
403 db.query(&Statement::new("SELECT 1"))
404 .await
405 .expect("queries");
406 }
407
408 assert_eq!(registry.open_pools(), 1);
409 assert_eq!(
410 dsns.lookups(),
411 vec!["a".to_owned()],
412 "the DSN is read once, not per request"
413 );
414 temp.finish().await;
415 }
416
417 #[tokio::test]
418 async fn an_idle_pool_is_closed_and_reopens_on_the_next_request() {
419 let Some(url) = base_url() else {
420 eprintln!("SKIPPED: {}", skip_reason());
421 return;
422 };
423 let Some(temp) = TempDb::create(&url, "regidle").await else {
424 eprintln!("SKIPPED: {}", skip_reason());
425 return;
426 };
427 let dsns = Dsns::new(&temp.url, &["a"]);
428 let clock = StepClock::new();
429 let registry = PoolRegistry::with_limits(
430 Arc::clone(&dsns) as Arc<dyn TenantDsns>,
431 Arc::clone(&clock) as Arc<dyn Clock>,
432 PoolLimits::default(),
433 8,
434 Duration::from_secs(60),
435 );
436
437 registry.pool_for("a").await.expect("opens");
438 assert_eq!(registry.open_pools(), 1);
439
440 clock.advance(120);
441 registry.pool_for("a").await.expect("reopens");
442 assert_eq!(registry.open_pools(), 1, "reopened, not accumulated");
443 assert_eq!(
444 dsns.lookups().len(),
445 2,
446 "a reopen reads the DSN again, which is what makes eviction real"
447 );
448 temp.finish().await;
449 }
450
451 #[tokio::test]
452 async fn one_key_cannot_starve_the_others_of_the_total() {
453 let Some(url) = base_url() else {
457 eprintln!("SKIPPED: {}", skip_reason());
458 return;
459 };
460 let Some(temp) = TempDb::create(&url, "regstarve").await else {
461 eprintln!("SKIPPED: {}", skip_reason());
462 return;
463 };
464 let dsns = Dsns::new(&temp.url, &["a", "b"]);
465 let registry = Arc::new(PoolRegistry::with_limits(
466 Arc::clone(&dsns) as Arc<dyn TenantDsns>,
467 StepClock::new(),
468 PoolLimits {
469 max_connections: 4,
470 idle_timeout: None,
471 acquire_timeout: Duration::from_millis(400),
472 },
473 1,
474 Duration::from_secs(300),
475 ));
476
477 let a = registry.pool_for("a").await.expect("a opens");
478 let b = registry.pool_for("b").await.expect("b opens");
479
480 let hold = tokio::spawn(async move {
481 let _ = a.query(&Statement::new("SELECT pg_sleep(5)")).await;
482 });
483
484 let contended = async {
493 loop {
494 if b.query(&Statement::new("SELECT 1")).await.is_err() {
495 return;
496 }
497 tokio::time::sleep(Duration::from_millis(20)).await;
498 }
499 };
500 tokio::time::timeout(Duration::from_secs(4), contended)
501 .await
502 .expect("A holds the only permit within four seconds");
503
504 let started = std::time::Instant::now();
508 let refused = b.query(&Statement::new("SELECT 1")).await;
509 let waited = started.elapsed();
510
511 assert!(
512 refused.is_err(),
513 "B must be refused rather than queued behind A: {refused:?}"
514 );
515 assert!(
516 waited < Duration::from_secs(2),
517 "and refused on its own deadline, not after A finished: {waited:?}"
518 );
519 hold.abort();
520 let _ = hold.await;
521
522 b.query(&Statement::new("SELECT 1"))
524 .await
525 .expect("B recovers once the budget frees");
526 temp.finish().await;
527 }
528
529 #[tokio::test]
530 async fn an_unknown_key_opens_nothing_and_says_which_kind_of_miss() {
531 let Some(url) = base_url() else {
532 eprintln!("SKIPPED: {}", skip_reason());
533 return;
534 };
535 let Some(temp) = TempDb::create(&url, "regmissing").await else {
536 eprintln!("SKIPPED: {}", skip_reason());
537 return;
538 };
539 let dsns = Dsns::new(&temp.url, &["a"]);
540 let registry =
541 PoolRegistry::new(Arc::clone(&dsns) as Arc<dyn TenantDsns>, StepClock::new());
542
543 assert_eq!(
544 registry.pool_for("nobody").await.err(),
545 Some(PoolError::NoDsn)
546 );
547 assert_eq!(registry.open_pools(), 0, "and opened nothing");
548 temp.finish().await;
549 }
550}