mz_postgres_client/
lib.rs1#![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::sync::Arc;
26use std::sync::atomic::{AtomicU64, Ordering};
27use std::time::{Duration, Instant};
28
29use deadpool_postgres::tokio_postgres::Config;
30use deadpool_postgres::{
31 Hook, HookError, HookErrorCause, Manager, ManagerConfig, Object, Pool, PoolError,
32 RecyclingMethod, Runtime, Status,
33};
34use mz_ore::cast::{CastFrom, CastLossy};
35use mz_ore::now::SYSTEM_TIME;
36use mz_ore::url::SensitiveUrl;
37use tracing::debug;
38
39use crate::error::PostgresError;
40use crate::metrics::PostgresClientMetrics;
41
42pub trait PostgresClientKnobs: std::fmt::Debug + Send + Sync {
44 fn connection_pool_max_size(&self) -> usize;
46 fn connection_pool_max_wait(&self) -> Option<Duration>;
48 fn connection_pool_ttl(&self) -> Duration;
51 fn connection_pool_ttl_stagger(&self) -> Duration;
54 fn connect_timeout(&self) -> Duration;
56 fn tcp_user_timeout(&self) -> Duration;
58 fn keepalives_idle(&self) -> Duration;
60 fn keepalives_interval(&self) -> Duration;
62 fn keepalives_retries(&self) -> u32;
64 fn statement_timeout(&self) -> Duration;
67}
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub enum IsolationLevel {
72 Serializable,
74 ReadCommitted,
77}
78
79impl IsolationLevel {
80 fn set_characteristics_sql(self) -> &'static str {
82 match self {
83 IsolationLevel::Serializable => {
84 "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE"
85 }
86 IsolationLevel::ReadCommitted => {
87 "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ COMMITTED"
88 }
89 }
90 }
91}
92
93pub type IsolationLevelFn = Arc<dyn Fn() -> IsolationLevel + Send + Sync>;
96
97#[derive(Clone)]
99pub struct PostgresClientConfig {
100 url: SensitiveUrl,
101 knobs: Arc<dyn PostgresClientKnobs>,
102 metrics: PostgresClientMetrics,
103 isolation: IsolationLevelFn,
104}
105
106impl std::fmt::Debug for PostgresClientConfig {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 f.debug_struct("PostgresClientConfig")
109 .field("url", &self.url)
110 .finish_non_exhaustive()
111 }
112}
113
114impl PostgresClientConfig {
115 pub fn new(
118 url: SensitiveUrl,
119 knobs: Arc<dyn PostgresClientKnobs>,
120 metrics: PostgresClientMetrics,
121 ) -> Self {
122 PostgresClientConfig {
123 url,
124 knobs,
125 metrics,
126 isolation: Arc::new(|| IsolationLevel::Serializable),
127 }
128 }
129
130 pub fn with_isolation(mut self, isolation: IsolationLevelFn) -> Self {
132 self.isolation = isolation;
133 self
134 }
135}
136
137pub struct PostgresClient {
139 pool: Pool,
140 metrics: PostgresClientMetrics,
141}
142
143impl std::fmt::Debug for PostgresClient {
144 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145 f.debug_struct("PostgresClient").finish_non_exhaustive()
146 }
147}
148
149impl PostgresClient {
150 pub fn open(config: PostgresClientConfig) -> Result<Self, PostgresError> {
152 let mut pg_config: Config = config.url.to_string_unredacted().parse()?;
153 pg_config.connect_timeout(config.knobs.connect_timeout());
154 pg_config.tcp_user_timeout(config.knobs.tcp_user_timeout());
155
156 pg_config.keepalives(true);
160 pg_config.keepalives_idle(config.knobs.keepalives_idle());
161 pg_config.keepalives_interval(config.knobs.keepalives_interval());
162 pg_config.keepalives_retries(config.knobs.keepalives_retries());
163
164 let tls = mz_tls_util::make_tls(&pg_config).map_err(|tls_err| match tls_err {
165 mz_tls_util::TlsError::Generic(e) => PostgresError::Indeterminate(e),
166 mz_tls_util::TlsError::OpenSsl(e) => PostgresError::Indeterminate(anyhow::anyhow!(e)),
167 })?;
168
169 let manager = Manager::from_config(
170 pg_config,
171 tls,
172 ManagerConfig {
173 recycling_method: RecyclingMethod::Fast,
174 },
175 );
176
177 let last_ttl_connection = AtomicU64::new(0);
178 let connections_created = config.metrics.connpool_connections_created.clone();
179 let ttl_reconnections = config.metrics.connpool_ttl_reconnections.clone();
180 let knobs = Arc::clone(&config.knobs);
181 let isolation = Arc::clone(&config.isolation);
182 let builder = Pool::builder(manager);
183 let builder = match config.knobs.connection_pool_max_wait() {
184 None => builder,
185 Some(wait) => builder.wait_timeout(Some(wait)).runtime(Runtime::Tokio1),
186 };
187 let pool = builder
188 .max_size(config.knobs.connection_pool_max_size())
189 .post_create(Hook::async_fn(move |client, _| {
190 connections_created.inc();
191 let knobs = Arc::clone(&knobs);
192 let isolation = Arc::clone(&isolation);
195 Box::pin(async move {
196 debug!("opened new consensus postgres connection");
197 let mut setup = isolation().set_characteristics_sql().to_owned();
198 let statement_timeout = knobs.statement_timeout();
202 if !statement_timeout.is_zero() {
203 write!(
206 setup,
207 "; SET statement_timeout = {}",
208 statement_timeout.as_millis()
209 )
210 .expect("writing to a String never fails");
211 }
212 #[allow(clippy::disallowed_methods)]
215 client
216 .batch_execute(&setup)
217 .await
218 .map_err(|e| HookError::Abort(HookErrorCause::Backend(e)))
219 })
220 }))
221 .pre_recycle(Hook::sync_fn(move |_client, conn_metrics| {
222 if conn_metrics.age() < config.knobs.connection_pool_ttl() {
229 return Ok(());
230 }
231
232 let last_ttl = last_ttl_connection.load(Ordering::SeqCst);
233 let now = (SYSTEM_TIME)();
234 let elapsed_since_last_ttl = Duration::from_millis(now.saturating_sub(last_ttl));
235
236 if elapsed_since_last_ttl > config.knobs.connection_pool_ttl_stagger()
238 && last_ttl_connection
239 .compare_exchange_weak(last_ttl, now, Ordering::SeqCst, Ordering::SeqCst)
240 .is_ok()
241 {
242 ttl_reconnections.inc();
243 return Err(HookError::Continue(Some(HookErrorCause::Message(
244 "connection has been TTLed".to_string(),
245 ))));
246 }
247
248 Ok(())
249 }))
250 .build()
251 .expect("postgres connection pool built with incorrect parameters");
252
253 Ok(PostgresClient {
254 pool,
255 metrics: config.metrics,
256 })
257 }
258
259 fn status_metrics(&self, status: Status) {
260 self.metrics
261 .connpool_available
262 .set(f64::cast_lossy(status.available));
263 self.metrics.connpool_size.set(u64::cast_from(status.size));
264 }
266
267 pub async fn get_connection(&self) -> Result<Object, PoolError> {
269 let start = Instant::now();
270 self.status_metrics(self.pool.status());
272 let res = self.pool.get().await;
273 if let Err(PoolError::Backend(err)) = &res {
274 debug!("error establishing connection: {}", err);
275 self.metrics.connpool_connection_errors.inc();
276 }
277 self.metrics
278 .connpool_acquire_seconds
279 .inc_by(start.elapsed().as_secs_f64());
280 self.metrics.connpool_acquires.inc();
281 self.status_metrics(self.pool.status());
282 res
283 }
284}