1#![warn(missing_docs, missing_debug_implementations)]
14#![warn(
15 clippy::cast_possible_truncation,
16 clippy::cast_precision_loss,
17 clippy::cast_sign_loss,
18 clippy::clone_on_ref_ptr
19)]
20
21pub mod error;
22pub mod metrics;
23
24use std::fmt::Write;
25use std::ops::{Deref, DerefMut};
26use std::sync::Arc;
27use std::sync::atomic::{AtomicU64, Ordering};
28use std::time::{Duration, Instant};
29
30use deadpool::managed::{self, Hook, HookError, Metrics, Object, Pool, RecycleResult};
31use deadpool_postgres::tokio_postgres::{self, Config};
32use deadpool_postgres::{
33 ClientWrapper as DeadpoolClient, Manager as PgManager, ManagerConfig, PoolError,
34 RecyclingMethod, Runtime, Status,
35};
36use mz_ore::cast::{CastFrom, CastLossy};
37use mz_ore::metrics::Counter;
38use mz_ore::now::SYSTEM_TIME;
39use mz_ore::url::SensitiveUrl;
40use tracing::debug;
41
42use crate::error::PostgresError;
43use crate::metrics::PostgresClientMetrics;
44
45pub trait PostgresClientKnobs: std::fmt::Debug + Send + Sync {
47 fn connection_pool_max_size(&self) -> usize;
49 fn connection_pool_max_wait(&self) -> Option<Duration>;
51 fn connection_pool_ttl(&self) -> Duration;
54 fn connection_pool_ttl_stagger(&self) -> Duration;
57 fn connect_timeout(&self) -> Duration;
59 fn tcp_user_timeout(&self) -> Duration;
61 fn keepalives_idle(&self) -> Duration;
63 fn keepalives_interval(&self) -> Duration;
65 fn keepalives_retries(&self) -> u32;
67 fn statement_timeout(&self) -> Duration;
70}
71
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74pub enum IsolationLevel {
75 Serializable,
77 ReadCommitted,
80}
81
82impl IsolationLevel {
83 fn set_characteristics_sql(self) -> &'static str {
85 match self {
86 IsolationLevel::Serializable => {
87 "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE"
88 }
89 IsolationLevel::ReadCommitted => {
90 "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ COMMITTED"
91 }
92 }
93 }
94}
95
96pub type IsolationLevelFn = Arc<dyn Fn() -> IsolationLevel + Send + Sync>;
99
100pub type Connection = Object<Manager>;
103
104#[derive(Debug)]
108pub struct Client {
109 inner: DeadpoolClient,
110 isolation: IsolationLevel,
111}
112
113impl Client {
114 pub fn isolation_level(&self) -> IsolationLevel {
116 self.isolation
117 }
118}
119
120impl Deref for Client {
121 type Target = DeadpoolClient;
122
123 fn deref(&self) -> &DeadpoolClient {
124 &self.inner
125 }
126}
127
128impl DerefMut for Client {
129 fn deref_mut(&mut self) -> &mut DeadpoolClient {
130 &mut self.inner
131 }
132}
133
134pub struct Manager {
138 inner: PgManager,
139 isolation: IsolationLevelFn,
142 knobs: Arc<dyn PostgresClientKnobs>,
143 connections_created: Counter,
144}
145
146impl std::fmt::Debug for Manager {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 f.debug_struct("Manager")
149 .field("inner", &self.inner)
150 .finish_non_exhaustive()
151 }
152}
153
154impl managed::Manager for Manager {
155 type Type = Client;
156 type Error = tokio_postgres::Error;
157
158 async fn create(&self) -> Result<Client, tokio_postgres::Error> {
159 let inner = self.inner.create().await?;
160 self.connections_created.inc();
161
162 let isolation = (self.isolation)();
165 let mut setup = isolation.set_characteristics_sql().to_owned();
166 let statement_timeout = self.knobs.statement_timeout();
169 if !statement_timeout.is_zero() {
170 write!(
172 setup,
173 "; SET statement_timeout = {}",
174 statement_timeout.as_millis()
175 )
176 .expect("writing to a String never fails");
177 }
178 debug!("opened new postgres connection");
179 #[allow(clippy::disallowed_methods)]
182 inner.batch_execute(&setup).await?;
183
184 Ok(Client { inner, isolation })
185 }
186
187 async fn recycle(
188 &self,
189 client: &mut Client,
190 metrics: &Metrics,
191 ) -> RecycleResult<tokio_postgres::Error> {
192 self.inner.recycle(&mut client.inner, metrics).await
193 }
194
195 fn detach(&self, client: &mut Client) {
196 self.inner.detach(&mut client.inner)
197 }
198}
199
200#[derive(Clone)]
202pub struct PostgresClientConfig {
203 url: SensitiveUrl,
204 knobs: Arc<dyn PostgresClientKnobs>,
205 metrics: PostgresClientMetrics,
206 isolation: IsolationLevelFn,
207}
208
209impl std::fmt::Debug for PostgresClientConfig {
210 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211 f.debug_struct("PostgresClientConfig")
212 .field("url", &self.url)
213 .finish_non_exhaustive()
214 }
215}
216
217impl PostgresClientConfig {
218 pub fn new(
221 url: SensitiveUrl,
222 knobs: Arc<dyn PostgresClientKnobs>,
223 metrics: PostgresClientMetrics,
224 ) -> Self {
225 PostgresClientConfig {
226 url,
227 knobs,
228 metrics,
229 isolation: Arc::new(|| IsolationLevel::Serializable),
230 }
231 }
232
233 pub fn with_isolation(mut self, isolation: IsolationLevelFn) -> Self {
235 self.isolation = isolation;
236 self
237 }
238}
239
240pub struct PostgresClient {
242 pool: Pool<Manager>,
243 knobs: Arc<dyn PostgresClientKnobs>,
244 metrics: PostgresClientMetrics,
245}
246
247impl std::fmt::Debug for PostgresClient {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 f.debug_struct("PostgresClient").finish_non_exhaustive()
250 }
251}
252
253impl PostgresClient {
254 pub fn open(config: PostgresClientConfig) -> Result<Self, PostgresError> {
256 let mut pg_config: Config = config.url.to_string_unredacted().parse()?;
257 pg_config.connect_timeout(config.knobs.connect_timeout());
258 pg_config.tcp_user_timeout(config.knobs.tcp_user_timeout());
259
260 pg_config.keepalives(true);
264 pg_config.keepalives_idle(config.knobs.keepalives_idle());
265 pg_config.keepalives_interval(config.knobs.keepalives_interval());
266 pg_config.keepalives_retries(config.knobs.keepalives_retries());
267
268 let tls = mz_tls_util::make_tls(&pg_config).map_err(|tls_err| match tls_err {
269 mz_tls_util::TlsError::Generic(e) => PostgresError::Indeterminate(e),
270 mz_tls_util::TlsError::OpenSsl(e) => PostgresError::Indeterminate(anyhow::anyhow!(e)),
271 })?;
272
273 let pg_manager = PgManager::from_config(
274 pg_config,
275 tls,
276 ManagerConfig {
277 recycling_method: RecyclingMethod::Fast,
278 },
279 );
280 let manager = Manager {
283 inner: pg_manager,
284 isolation: Arc::clone(&config.isolation),
285 knobs: Arc::clone(&config.knobs),
286 connections_created: config.metrics.connpool_connections_created.clone(),
287 };
288
289 let last_ttl_connection = AtomicU64::new(0);
290 let ttl_reconnections = config.metrics.connpool_ttl_reconnections.clone();
291 let knobs = Arc::clone(&config.knobs);
292 let builder = Pool::builder(manager);
293 let builder = match config.knobs.connection_pool_max_wait() {
294 None => builder,
295 Some(wait) => builder.wait_timeout(Some(wait)).runtime(Runtime::Tokio1),
296 };
297 let pool = builder
298 .max_size(config.knobs.connection_pool_max_size())
299 .pre_recycle(Hook::sync_fn(move |_client, conn_metrics| {
300 if conn_metrics.age() < config.knobs.connection_pool_ttl() {
307 return Ok(());
308 }
309
310 let last_ttl = last_ttl_connection.load(Ordering::SeqCst);
311 let now = (SYSTEM_TIME)();
312 let elapsed_since_last_ttl = Duration::from_millis(now.saturating_sub(last_ttl));
313
314 if elapsed_since_last_ttl > config.knobs.connection_pool_ttl_stagger()
316 && last_ttl_connection
317 .compare_exchange_weak(last_ttl, now, Ordering::SeqCst, Ordering::SeqCst)
318 .is_ok()
319 {
320 ttl_reconnections.inc();
321 return Err(HookError::message("connection has been TTLed"));
324 }
325
326 Ok(())
327 }))
328 .build()
329 .expect("postgres connection pool built with incorrect parameters");
330
331 Ok(PostgresClient {
332 pool,
333 knobs,
334 metrics: config.metrics,
335 })
336 }
337
338 fn status_metrics(&self, status: Status) {
339 self.metrics
341 .connpool_available
342 .set(f64::cast_lossy(status.available) - f64::cast_lossy(status.waiting));
343 self.metrics.connpool_size.set(u64::cast_from(status.size));
344 }
346
347 pub fn status(&self) -> Status {
351 self.pool.status()
352 }
353
354 pub async fn get_connection(&self) -> Result<Connection, PoolError> {
356 let start = Instant::now();
357 let status = self.pool.status();
359 let max_size = self.knobs.connection_pool_max_size();
364 if status.max_size != max_size {
365 self.pool.resize(max_size);
366 }
367 self.status_metrics(status);
368 let res = self.pool.get().await;
369 if let Err(PoolError::Backend(err)) = &res {
370 debug!("error establishing connection: {}", err);
371 self.metrics.connpool_connection_errors.inc();
372 }
373 self.metrics
374 .connpool_acquire_seconds
375 .inc_by(start.elapsed().as_secs_f64());
376 self.metrics.connpool_acquires.inc();
377 self.status_metrics(self.pool.status());
378 res
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use std::str::FromStr;
385 use std::sync::atomic::AtomicUsize;
386
387 use mz_ore::metrics::MetricsRegistry;
388
389 use super::*;
390
391 #[derive(Debug)]
392 struct TestKnobs {
393 max_size: AtomicUsize,
394 }
395
396 impl PostgresClientKnobs for TestKnobs {
397 fn connection_pool_max_size(&self) -> usize {
398 self.max_size.load(Ordering::SeqCst)
399 }
400 fn connection_pool_max_wait(&self) -> Option<Duration> {
401 Some(Duration::from_secs(1))
402 }
403 fn connection_pool_ttl(&self) -> Duration {
404 Duration::MAX
405 }
406 fn connection_pool_ttl_stagger(&self) -> Duration {
407 Duration::MAX
408 }
409 fn connect_timeout(&self) -> Duration {
410 Duration::from_secs(5)
411 }
412 fn tcp_user_timeout(&self) -> Duration {
413 Duration::ZERO
414 }
415 fn keepalives_idle(&self) -> Duration {
416 Duration::from_secs(10)
417 }
418 fn keepalives_interval(&self) -> Duration {
419 Duration::from_secs(5)
420 }
421 fn keepalives_retries(&self) -> u32 {
422 5
423 }
424 fn statement_timeout(&self) -> Duration {
425 Duration::ZERO
426 }
427 }
428
429 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
436 async fn pool_resize_applies_on_acquire() {
437 let url = match std::env::var("MZ_PERSIST_EXTERNAL_STORAGE_TEST_POSTGRES_URL") {
438 Ok(url) => SensitiveUrl::from_str(&url).expect("valid url"),
439 Err(_) => return,
440 };
441 let knobs = Arc::new(TestKnobs {
442 max_size: AtomicUsize::new(2),
443 });
444 let dyn_knobs: Arc<dyn PostgresClientKnobs> = Arc::<TestKnobs>::clone(&knobs);
445 let config = PostgresClientConfig::new(
446 url,
447 dyn_knobs,
448 PostgresClientMetrics::new(&MetricsRegistry::new(), "mz_postgres_client_test"),
449 );
450 let client = PostgresClient::open(config).expect("open client");
451
452 let conn = client.get_connection().await.expect("connection");
453 assert_eq!(client.status().max_size, 2);
454
455 knobs.max_size.store(5, Ordering::SeqCst);
457 let conn2 = client.get_connection().await.expect("connection");
458 assert_eq!(client.status().max_size, 5);
459
460 knobs.max_size.store(1, Ordering::SeqCst);
463 drop(conn);
464 drop(conn2);
465 let _conn3 = client.get_connection().await.expect("connection");
466 assert_eq!(client.status().max_size, 1);
467 }
468}