-
Notifications
You must be signed in to change notification settings - Fork 282
Pool Postgres connections #3043
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,20 +1,85 @@ | ||||||||||||
use anyhow::{anyhow, Result}; | ||||||||||||
use anyhow::{anyhow, Context, Result}; | ||||||||||||
use native_tls::TlsConnector; | ||||||||||||
use postgres_native_tls::MakeTlsConnector; | ||||||||||||
use spin_world::async_trait; | ||||||||||||
use spin_world::spin::postgres::postgres::{ | ||||||||||||
self as v3, Column, DbDataType, DbValue, ParameterValue, RowSet, | ||||||||||||
}; | ||||||||||||
use tokio_postgres::types::Type; | ||||||||||||
use tokio_postgres::{config::SslMode, types::ToSql, Row}; | ||||||||||||
use tokio_postgres::{Client as TokioClient, NoTls, Socket}; | ||||||||||||
use tokio_postgres::{config::SslMode, types::ToSql, NoTls, Row}; | ||||||||||||
|
||||||||||||
const CONNECTION_POOL_SIZE: usize = 64; | ||||||||||||
|
||||||||||||
#[async_trait] | ||||||||||||
pub trait Client { | ||||||||||||
async fn build_client(address: &str) -> Result<Self> | ||||||||||||
where | ||||||||||||
Self: Sized; | ||||||||||||
pub trait ClientFactory: Send + Sync { | ||||||||||||
type Client: Client + Send + Sync + 'static; | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know it didn't have doc comments before, but it would be awesome if it gained some. |
||||||||||||
fn new() -> Self; | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess we could probably move these constraints onto |
||||||||||||
async fn build_client(&mut self, address: &str) -> Result<Self::Client>; | ||||||||||||
Comment on lines
+14
to
+16
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would let you
Suggested change
|
||||||||||||
} | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: would this be better named
Suggested change
|
||||||||||||
|
||||||||||||
pub struct PooledTokioClientFactory { | ||||||||||||
pools: std::collections::HashMap<String, deadpool_postgres::Pool>, | ||||||||||||
} | ||||||||||||
|
||||||||||||
#[async_trait] | ||||||||||||
impl ClientFactory for PooledTokioClientFactory { | ||||||||||||
type Client = deadpool_postgres::Object; | ||||||||||||
|
||||||||||||
fn new() -> Self { | ||||||||||||
Self { | ||||||||||||
pools: Default::default(), | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
async fn build_client(&mut self, address: &str) -> Result<Self::Client> { | ||||||||||||
let pool_entry = self.pools.entry(address.to_owned()); | ||||||||||||
let pool = match pool_entry { | ||||||||||||
std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(), | ||||||||||||
std::collections::hash_map::Entry::Vacant(entry) => { | ||||||||||||
let pool = create_connection_pool(address) | ||||||||||||
.context("establishing PostgreSQL connection pool")?; | ||||||||||||
entry.insert(pool) | ||||||||||||
} | ||||||||||||
}; | ||||||||||||
|
||||||||||||
Ok(pool.get().await?) | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
fn create_connection_pool(address: &str) -> Result<deadpool_postgres::Pool> { | ||||||||||||
let config = address | ||||||||||||
.parse::<tokio_postgres::Config>() | ||||||||||||
.context("parsing Postgres connection string")?; | ||||||||||||
|
||||||||||||
tracing::debug!("Build new connection: {}", address); | ||||||||||||
|
||||||||||||
// TODO: This is slower but safer. Is it the right tradeoff? | ||||||||||||
// https://docs.rs/deadpool-postgres/latest/deadpool_postgres/enum.RecyclingMethod.html | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is a good default. We can always expose other options via runtime config later if we want. |
||||||||||||
let mgr_config = deadpool_postgres::ManagerConfig { | ||||||||||||
recycling_method: deadpool_postgres::RecyclingMethod::Clean, | ||||||||||||
}; | ||||||||||||
|
||||||||||||
let mgr = if config.get_ssl_mode() == SslMode::Disable { | ||||||||||||
deadpool_postgres::Manager::from_config(config, NoTls, mgr_config) | ||||||||||||
} else { | ||||||||||||
let builder = TlsConnector::builder(); | ||||||||||||
let connector = MakeTlsConnector::new(builder.build()?); | ||||||||||||
deadpool_postgres::Manager::from_config(config, connector, mgr_config) | ||||||||||||
}; | ||||||||||||
|
||||||||||||
// TODO: what is our max size heuristic? Should this be passed in soe that different | ||||||||||||
// hosts can manage it according to their needs? Will a plain number suffice for | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit:
Suggested change
|
||||||||||||
// sophisticated hosts anyway? | ||||||||||||
let pool = deadpool_postgres::Pool::builder(mgr) | ||||||||||||
.max_size(CONNECTION_POOL_SIZE) | ||||||||||||
.build() | ||||||||||||
.context("building Postgres connection pool")?; | ||||||||||||
|
||||||||||||
Ok(pool) | ||||||||||||
} | ||||||||||||
|
||||||||||||
#[async_trait] | ||||||||||||
pub trait Client { | ||||||||||||
async fn execute( | ||||||||||||
&self, | ||||||||||||
statement: String, | ||||||||||||
|
@@ -29,28 +94,7 @@ pub trait Client { | |||||||||||
} | ||||||||||||
|
||||||||||||
#[async_trait] | ||||||||||||
impl Client for TokioClient { | ||||||||||||
async fn build_client(address: &str) -> Result<Self> | ||||||||||||
where | ||||||||||||
Self: Sized, | ||||||||||||
{ | ||||||||||||
let config = address.parse::<tokio_postgres::Config>()?; | ||||||||||||
|
||||||||||||
tracing::debug!("Build new connection: {}", address); | ||||||||||||
|
||||||||||||
if config.get_ssl_mode() == SslMode::Disable { | ||||||||||||
let (client, connection) = config.connect(NoTls).await?; | ||||||||||||
spawn_connection(connection); | ||||||||||||
Ok(client) | ||||||||||||
} else { | ||||||||||||
let builder = TlsConnector::builder(); | ||||||||||||
let connector = MakeTlsConnector::new(builder.build()?); | ||||||||||||
let (client, connection) = config.connect(connector).await?; | ||||||||||||
spawn_connection(connection); | ||||||||||||
Ok(client) | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
impl Client for deadpool_postgres::Object { | ||||||||||||
async fn execute( | ||||||||||||
&self, | ||||||||||||
statement: String, | ||||||||||||
|
@@ -67,7 +111,8 @@ impl Client for TokioClient { | |||||||||||
.map(|b| b.as_ref() as &(dyn ToSql + Sync)) | ||||||||||||
.collect(); | ||||||||||||
|
||||||||||||
self.execute(&statement, params_refs.as_slice()) | ||||||||||||
self.as_ref() | ||||||||||||
.execute(&statement, params_refs.as_slice()) | ||||||||||||
.await | ||||||||||||
.map_err(|e| v3::Error::QueryFailed(format!("{e:?}"))) | ||||||||||||
} | ||||||||||||
|
@@ -89,6 +134,7 @@ impl Client for TokioClient { | |||||||||||
.collect(); | ||||||||||||
|
||||||||||||
let results = self | ||||||||||||
.as_ref() | ||||||||||||
.query(&statement, params_refs.as_slice()) | ||||||||||||
.await | ||||||||||||
.map_err(|e| v3::Error::QueryFailed(format!("{e:?}")))?; | ||||||||||||
|
@@ -111,17 +157,6 @@ impl Client for TokioClient { | |||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
fn spawn_connection<T>(connection: tokio_postgres::Connection<Socket, T>) | ||||||||||||
where | ||||||||||||
T: tokio_postgres::tls::TlsStream + std::marker::Unpin + std::marker::Send + 'static, | ||||||||||||
{ | ||||||||||||
tokio::spawn(async move { | ||||||||||||
if let Err(e) = connection.await { | ||||||||||||
tracing::error!("Postgres connection error: {}", e); | ||||||||||||
} | ||||||||||||
}); | ||||||||||||
} | ||||||||||||
|
||||||||||||
fn to_sql_parameter(value: &ParameterValue) -> Result<Box<dyn ToSql + Send + Sync>> { | ||||||||||||
match value { | ||||||||||||
ParameterValue::Boolean(v) => Ok(Box::new(*v)), | ||||||||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,24 +1,26 @@ | ||||||
pub mod client; | ||||||
mod host; | ||||||
|
||||||
use client::Client; | ||||||
use std::sync::Arc; | ||||||
|
||||||
use client::ClientFactory; | ||||||
use spin_factor_outbound_networking::{ | ||||||
config::allowed_hosts::OutboundAllowedHosts, OutboundNetworkingFactor, | ||||||
}; | ||||||
use spin_factors::{ | ||||||
anyhow, ConfigureAppContext, Factor, FactorData, PrepareContext, RuntimeFactors, | ||||||
SelfInstanceBuilder, | ||||||
}; | ||||||
use tokio_postgres::Client as PgClient; | ||||||
use tokio::sync::RwLock; | ||||||
|
||||||
pub struct OutboundPgFactor<C = PgClient> { | ||||||
_phantom: std::marker::PhantomData<C>, | ||||||
pub struct OutboundPgFactor<CF = crate::client::PooledTokioClientFactory> { | ||||||
_phantom: std::marker::PhantomData<CF>, | ||||||
} | ||||||
|
||||||
impl<C: Send + Sync + Client + 'static> Factor for OutboundPgFactor<C> { | ||||||
impl<CF: ClientFactory + Send + Sync + 'static> Factor for OutboundPgFactor<CF> { | ||||||
type RuntimeConfig = (); | ||||||
type AppState = (); | ||||||
type InstanceBuilder = InstanceState<C>; | ||||||
type AppState = Arc<RwLock<CF>>; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd recommend leaving interior mutability up to the trait implementation if possible; some implementations might not need to lock.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would require changing the trait to use immutable borrows which is fine, but it will complicate the implementation of the ClientFactory.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have deleted several stream-of-consciousness comments about this to summarize here: There is a performance hazard with the current impl: this lock is held for the duration of the async call to I don't know how much this problem would apply to Pushing interior mutability down into the |
||||||
type InstanceBuilder = InstanceState<CF>; | ||||||
|
||||||
fn init(&mut self, ctx: &mut impl spin_factors::InitContext<Self>) -> anyhow::Result<()> { | ||||||
ctx.link_bindings(spin_world::v1::postgres::add_to_linker::<_, FactorData<Self>>)?; | ||||||
|
@@ -33,7 +35,7 @@ impl<C: Send + Sync + Client + 'static> Factor for OutboundPgFactor<C> { | |||||
&self, | ||||||
_ctx: ConfigureAppContext<T, Self>, | ||||||
) -> anyhow::Result<Self::AppState> { | ||||||
Ok(()) | ||||||
Ok(Arc::new(RwLock::new(CF::new()))) | ||||||
} | ||||||
|
||||||
fn prepare<T: RuntimeFactors>( | ||||||
|
@@ -45,6 +47,7 @@ impl<C: Send + Sync + Client + 'static> Factor for OutboundPgFactor<C> { | |||||
.allowed_hosts(); | ||||||
Ok(InstanceState { | ||||||
allowed_hosts, | ||||||
client_factory: ctx.app_state().clone(), | ||||||
connections: Default::default(), | ||||||
}) | ||||||
} | ||||||
|
@@ -64,9 +67,10 @@ impl<C> OutboundPgFactor<C> { | |||||
} | ||||||
} | ||||||
|
||||||
pub struct InstanceState<C> { | ||||||
pub struct InstanceState<CF: ClientFactory> { | ||||||
allowed_hosts: OutboundAllowedHosts, | ||||||
connections: spin_resource_table::Table<C>, | ||||||
client_factory: Arc<RwLock<CF>>, | ||||||
connections: spin_resource_table::Table<CF::Client>, | ||||||
} | ||||||
|
||||||
impl<C: Send + 'static> SelfInstanceBuilder for InstanceState<C> {} | ||||||
impl<CF: ClientFactory + Send + 'static> SelfInstanceBuilder for InstanceState<CF> {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🏭
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should be safe to add
+ 'static
here, which would let us remove all the relatedSend + Sync + 'static
constraints elsewhere.