use std::{collections::HashMap, sync::Arc, time::Duration}; use anyhow::Result; use axum::extract::FromRef; use derive_builder::Builder; use phono_backends::{client::WorkspaceClient, rolnames::ROLE_PREFIX_USER}; use phono_models::{client::AppDbClient, workspace::Workspace}; use sqlx::{Executor, PgPool, postgres::PgPoolOptions, raw_sql}; use tokio::sync::{OnceCell, RwLock}; use uuid::Uuid; use crate::app::App; const MAX_CONNECTIONS: u32 = 4; const IDLE_SECONDS: u64 = 3600; /// A collection of multiple SQLx Pools. #[derive(Builder, Clone, Debug)] pub struct WorkspacePooler { #[builder(default, setter(skip))] pools: Arc>>>, app_db_pool: PgPool, } impl WorkspacePooler { pub fn builder() -> WorkspacePoolerBuilder { WorkspacePoolerBuilder::default() } async fn get_pool_for(&mut self, workspace_id: Uuid) -> Result { let init_cell = || async { let mut app_db = AppDbClient::from_pool_conn(self.app_db_pool.acquire().await?); let workspace = Workspace::with_id(workspace_id) .fetch_one(&mut app_db) .await?; Ok(PgPoolOptions::new() .min_connections(0) .max_connections(MAX_CONNECTIONS) .idle_timeout(Some(Duration::from_secs(IDLE_SECONDS))) .after_release(|conn, _| { Box::pin(async move { // Essentially "DISCARD ALL" without "DEALLOCATE ALL" conn.execute(raw_sql( r#" close all; set session authorization default; reset all; unlisten *; select pg_advisory_unlock_all(); discard plans; discard temp; discard sequences; "#, )) .await?; Ok(true) }) }) .connect( workspace .fetch_cluster(&mut app_db) .await? .conn_str_for_db(&workspace.db_name, None)? .expose_secret() .as_str(), ) .await?) }; // Attempt to get an existing pool without write-locking the map let pools = self.pools.read().await; if let Some(cell) = pools.get(&workspace_id) { return Ok(cell .get_or_try_init::(init_cell) .await? .clone()); } drop(pools); // Release read lock let mut pools = self.pools.write().await; let entry = pools.entry(workspace_id).or_insert(OnceCell::new()); Ok(entry .get_or_try_init::(init_cell) .await? .clone()) } /// Note that while using `SET ROLE` simulates impersonation for most data /// access and RLS purposes, it is both incomplete and easily reversible: /// some commands and system tables will still behave according to the /// privileges of the session user, and clients relying on this abstraction /// should **NEVER** execute untrusted SQL. pub async fn acquire_for( &mut self, workspace_id: Uuid, set_role: RoleAssignment, ) -> Result { let pool = self.get_pool_for(workspace_id).await?; let mut client = WorkspaceClient::from_pool_conn(pool.acquire().await?); match set_role { RoleAssignment::User(uid) => { // TODO: Return error if user does not have "CONNECT" privileges // on backing database. Note that this change will entail a // fairly broad refactor of [`phono-server`] code for // initializing user roles and for performing workspace auth // checks. let rolname = format!("{ROLE_PREFIX_USER}{uid}", uid = uid.simple()); client.ensure_role(&rolname).await?; client.set_role(&rolname).await?; } RoleAssignment::Root => { /* no-op */ } } Ok(client) } pub async fn close_for(&mut self, workspace_id: Uuid) -> Result<()> { let pools = self.pools.read().await; if let Some(cell) = pools.get(&workspace_id) && let Some(pool) = cell.get() { let pool = pool.clone(); drop(pools); // Release read lock let mut pools = self.pools.write().await; pools.remove(&workspace_id); drop(pools); // Release write lock pool.close().await; } Ok(()) } // TODO: Add a cleanup method to remove entries with no connections } impl FromRef for WorkspacePooler { fn from_ref(state: &App) -> Self { state.workspace_pooler.clone() } } pub enum RoleAssignment { Root, User(Uuid), }