use std::time::Duration;
use async_trait::async_trait;
use deadpool_postgres::{Config, ManagerConfig, Object, Pool, PoolConfig, PoolError, Runtime};
use log::{debug, error, warn};
use serde_json::{json, Value};
use talos_certifier::{
model::DecisionMessage,
ports::{
common::SharedPortTraits,
errors::{DecisionStoreError, DecisionStoreErrorKind},
DecisionStore,
},
};
use tokio_postgres::NoTls;
use crate::{PgConfig, PgError};
use super::utils::{get_uuid_key, parse_json_column};
#[derive(Clone)]
pub struct Pg {
pub pool: Pool,
pub max_retries: u32,
}
impl Pg {
pub async fn new(pg_config: PgConfig) -> Result<Self, PgError> {
let mut config = Config::new();
config.dbname = Some(pg_config.database);
config.user = Some(pg_config.user);
config.password = Some(pg_config.password);
config.host = Some(pg_config.host);
config.port = Some(pg_config.port.parse::<u16>().expect("Failed to parse port to u16"));
config.manager = Some(ManagerConfig {
recycling_method: deadpool_postgres::RecyclingMethod::Fast,
});
if let Some(pool_max_size) = pg_config.pool_size {
let pool_config = PoolConfig {
max_size: pool_max_size as usize,
..PoolConfig::default()
};
config.pool = Some(pool_config);
}
let pool = config.create_pool(Some(Runtime::Tokio1), NoTls).map_err(PgError::CreatePool)?;
let _ = pool.get().await.map_err(PgError::GetClientFromPool)?;
Ok(Pg {
pool,
max_retries: pg_config.max_retries.unwrap_or(5_u32),
})
}
pub async fn get_client(&self) -> Result<Object, PgError> {
let client = self.pool.get().await.map_err(PgError::GetClientFromPool)?;
Ok(client)
}
pub async fn get_client_with_retry(&self) -> Result<Object, PgError> {
let mut interval = tokio::time::interval(Duration::from_millis(5_000));
loop {
let result = self.pool.get().await;
match result {
Ok(pool_object) => return Ok(pool_object),
Err(pool_error) => match pool_error {
PoolError::Backend(_) | PoolError::Timeout(_) => {
interval.tick().await;
warn!("Error retreiving pool object, retrying...");
continue;
}
_ => return Err(PgError::GetClientFromPool(pool_error)),
},
};
}
}
}
#[async_trait]
impl DecisionStore for Pg {
type Decision = DecisionMessage;
async fn get_decision(&self, key: String) -> Result<Option<Self::Decision>, DecisionStoreError> {
let client = self.get_client_with_retry().await.map_err(|e| DecisionStoreError {
kind: DecisionStoreErrorKind::ClientError,
reason: e.to_string(),
data: None,
})?;
let stmt = client.prepare_cached("SELECT xid, decision from xdb where xid = $1").await.unwrap();
let key_uuid = get_uuid_key(&key)?;
let rows = client.query_opt(&stmt, &[&key_uuid]).await.map_err(|e| DecisionStoreError {
kind: DecisionStoreErrorKind::GetDecision,
reason: e.to_string(),
data: Some(key.clone()),
})?;
let Some(row) = rows else {
return Ok(None);
};
let val = row.get::<&str, Option<Value>>("decision");
let Some(value) = val else {
return Ok(None);
};
Ok(Some(parse_json_column(&key, value)?))
}
async fn insert_decision(&self, key: String, decision: Self::Decision) -> Result<Self::Decision, DecisionStoreError> {
let mut current_count = 0;
let mut result_f: Result<DecisionMessage, DecisionStoreError> = Err(DecisionStoreError {
kind: DecisionStoreErrorKind::InsertDecision,
reason: format!("Max retries exhausted for key={key} to insert decision to XDB"),
data: Some(format!("{:?}", decision)),
});
while current_count <= self.max_retries {
let client_result = self.get_client_with_retry().await.map_err(|e| DecisionStoreError {
kind: DecisionStoreErrorKind::ClientError,
reason: format!("Failed to get client with error {}", e),
data: None,
});
if let Ok(client) = client_result {
let key_uuid = get_uuid_key(&key)?;
let stmt = client
.prepare_cached(
"WITH ins AS (
INSERT INTO xdb(xid, decision)
VALUES ($1, $2)
ON CONFLICT DO NOTHING
RETURNING xid, decision
)
SELECT * from ins
UNION
SELECT xid, decision from xdb where xid = $1",
)
.await
.map_err(|e| DecisionStoreError {
kind: DecisionStoreErrorKind::InsertDecision,
reason: format!("Failed to prepare the insert statement to XDB {}", e),
data: Some(key.clone()),
})?;
let result = client.query_one(&stmt, &[&key_uuid, &json!(decision)]).await;
match result {
Ok(row) => {
let decision = match row.get::<&str, Option<Value>>("decision") {
Some(value) => Ok(parse_json_column(&key, value)?),
_ => Err(DecisionStoreError {
kind: DecisionStoreErrorKind::NoRowReturned,
reason: "Insert did not return rows".to_owned(),
data: Some(key.clone()),
}),
};
result_f = decision;
debug!("Exiting from okay handle of decision insert result");
break;
}
Err(e) => {
result_f = Err(DecisionStoreError {
kind: DecisionStoreErrorKind::InsertDecision,
reason: format!("Failed to insert decision into XDB with error {}", e),
data: Some(key.clone()),
});
error!("{result_f:#?}");
}
};
} else if let Some(client_err) = client_result.err() {
warn!(
"Error getting connection from pool prior to inserting decision to XDB with reason {}",
client_err.to_string()
);
result_f = Err(client_err);
}
let sleep_duration_ms = 10 * 2u64.pow(current_count);
warn!("Retrying inserting to XDB after waiting for {sleep_duration_ms}");
tokio::time::sleep(Duration::from_millis(sleep_duration_ms)).await;
current_count += 1;
}
result_f
}
}
#[async_trait]
impl SharedPortTraits for Pg {
async fn is_healthy(&self) -> bool {
true
}
async fn shutdown(&self) -> bool {
self.pool.close();
self.pool.is_closed()
}
}