partner_chains_db_sync_data_sources/
data_sources.rs

1//! Helpers for configuring and creating a Postgres database connection
2use figment::Figment;
3use figment::providers::Env;
4use serde::Deserialize;
5#[cfg(feature = "block-source")]
6use sidechain_domain::mainchain_epoch::MainchainEpochConfig;
7pub use sqlx::PgPool;
8use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
9use std::error::Error;
10use std::fmt::Debug;
11use std::fmt::Formatter;
12use std::str::FromStr;
13
14/// Reads Cardano main chain epoch configuration from the environment.
15///
16/// See documentation of [MainchainEpochConfig::read_from_env] for the list of environment variables read.
17#[cfg(feature = "block-source")]
18pub fn read_mc_epoch_config() -> Result<MainchainEpochConfig, Box<dyn Error + Send + Sync>> {
19	Ok(MainchainEpochConfig::read_from_env()
20		.map_err(|e| format!("Failed to read main chain config: {}", e))?)
21}
22
23/// Postgres connection config used when creating a [PgPool].
24#[derive(Debug, Clone, Deserialize)]
25pub struct ConnectionConfig {
26	/// Postgres connection pool, eg. `postgres://postgres-user:postgres-password@db-sync-postgres-host:5432/db-sync-db`
27	pub(crate) db_sync_postgres_connection_string: SecretString,
28}
29
30impl ConnectionConfig {
31	/// Reads Postgres connection config from the environment
32	pub fn from_env() -> Result<Self, Box<dyn Error + Send + Sync + 'static>> {
33		let config: Self = Figment::new()
34			.merge(Env::raw())
35			.extract()
36			.map_err(|e| format!("Failed to read postgres data source connection: {e}"))?;
37		Ok(config)
38	}
39}
40
41#[derive(Clone, serde::Deserialize, Default)]
42pub(crate) struct SecretString(pub String);
43
44impl Debug for SecretString {
45	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
46		write!(f, "***")
47	}
48}
49
50async fn get_connection(
51	connection_string: &str,
52	acquire_timeout: std::time::Duration,
53) -> Result<PgPool, Box<dyn Error + Send + Sync + 'static>> {
54	let connect_options = PgConnectOptions::from_str(connection_string)?;
55	let pool = PgPoolOptions::new()
56		.max_connections(5)
57		.acquire_timeout(acquire_timeout)
58		.connect_with(connect_options.clone())
59		.await
60		.map_err(|e| {
61			PostgresConnectionError(
62				connect_options.get_host().to_string(),
63				connect_options.get_port(),
64				connect_options.get_database().unwrap_or("cexplorer").to_string(),
65				e.to_string(),
66			)
67			.to_string()
68		})?;
69	Ok(pool)
70}
71
72#[derive(Debug, Clone, thiserror::Error)]
73#[error("Could not connect to database: postgres://***:***@{0}:{1}/{2}; error: {3}")]
74struct PostgresConnectionError(String, u16, String, String);
75
76/// Returns a Postgres connection pool constructed using configuration read from environment
77///
78/// # Environment variables read:
79/// - `DB_SYNC_POSTGRES_CONNECTION_STRING`: Postgres connection pool, eg.
80///   `postgres://postgres-user:postgres-password@db-sync-postgres-host:5432/db-sync-db`
81pub async fn get_connection_from_env() -> Result<PgPool, Box<dyn Error + Send + Sync + 'static>> {
82	let config = ConnectionConfig::from_env()?;
83	get_connection(
84		config.db_sync_postgres_connection_string.0.as_str(),
85		std::time::Duration::from_secs(30),
86	)
87	.await
88}
89
90#[cfg(test)]
91mod tests {
92	use super::*;
93	use sqlx::Error::PoolTimedOut;
94
95	#[tokio::test]
96	async fn display_passwordless_connection_string_on_connection_error() {
97		let expected_connection_error = PostgresConnectionError(
98			"localhost".to_string(),
99			4432,
100			"cexplorer_test".to_string(),
101			PoolTimedOut.to_string(),
102		);
103		let test_connection_string = "postgres://postgres:randompsw@localhost:4432/cexplorer_test";
104		let actual_connection_error =
105			get_connection(test_connection_string, std::time::Duration::from_millis(1)).await;
106		assert_eq!(
107			expected_connection_error.to_string(),
108			actual_connection_error.unwrap_err().to_string()
109		);
110	}
111}