use std::sync::Arc;
use std::time::Duration;
use sayiir_core::codec::{self, CodecIdentity, Decoder, Encoder};
use sayiir_core::snapshot::WorkflowSnapshot;
use sayiir_core::snapshot_format;
use sayiir_persistence::BackendError;
use sqlx::postgres::PgPoolOptions;
use sqlx::{PgPool, Row};
use crate::error::PgError;
use crate::wakeup::WakeupListener;
const MIN_PG_MAJOR_VERSION: u32 = 13;
#[derive(Debug, Clone, Default)]
pub struct PoolOptions {
pub max_connections: Option<u32>,
pub min_connections: Option<u32>,
pub acquire_timeout: Option<Duration>,
pub idle_timeout: Option<Duration>,
pub max_lifetime: Option<Duration>,
pub statement_timeout: Option<Duration>,
pub idle_in_transaction_session_timeout: Option<Duration>,
}
#[derive(Clone)]
pub struct PostgresBackend<C> {
pub(crate) pool: PgPool,
pub(crate) codec: C,
pub(crate) wakeup: Arc<WakeupListener>,
}
impl<C> PostgresBackend<C>
where
C: Default,
{
pub async fn connect(url: &str) -> Result<Self, BackendError> {
Self::connect_with_options(url, PoolOptions::default()).await
}
pub async fn connect_with_options(
url: &str,
options: PoolOptions,
) -> Result<Self, BackendError> {
let mut builder = PgPoolOptions::new();
if let Some(n) = options.max_connections {
builder = builder.max_connections(n);
}
if let Some(n) = options.min_connections {
builder = builder.min_connections(n);
}
if let Some(d) = options.acquire_timeout {
builder = builder.acquire_timeout(d);
}
if let Some(d) = options.idle_timeout {
builder = builder.idle_timeout(d);
}
if let Some(d) = options.max_lifetime {
builder = builder.max_lifetime(d);
}
let stmt_to = options.statement_timeout;
let idle_tx_to = options.idle_in_transaction_session_timeout;
if stmt_to.is_some() || idle_tx_to.is_some() {
builder = builder.after_connect(move |conn, _meta| {
Box::pin(async move {
if let Some(d) = stmt_to {
sqlx::query("SELECT set_config('statement_timeout', $1, false)")
.bind(duration_to_ms(d).to_string())
.execute(&mut *conn)
.await?;
}
if let Some(d) = idle_tx_to {
sqlx::query(
"SELECT set_config('idle_in_transaction_session_timeout', $1, false)",
)
.bind(duration_to_ms(d).to_string())
.execute(&mut *conn)
.await?;
}
Ok(())
})
});
}
let pool = builder.connect(url).await.map_err(PgError)?;
Self::init(pool).await
}
pub async fn connect_with(pool: PgPool) -> Result<Self, BackendError> {
Self::init(pool).await
}
async fn init(pool: PgPool) -> Result<Self, BackendError> {
check_pg_version(&pool).await?;
tracing::info!("running postgres migrations");
sqlx::migrate!("./migrations")
.run(&pool)
.await
.map_err(|e| BackendError::Backend(format!("migration failed: {e}")))?;
let wakeup = WakeupListener::spawn(pool.clone());
tracing::info!("postgres backend ready");
Ok(Self {
pool,
codec: C::default(),
wakeup,
})
}
}
impl<C> PostgresBackend<C>
where
C: Encoder + CodecIdentity + codec::sealed::EncodeValue<WorkflowSnapshot>,
{
pub(crate) fn encode(&self, snapshot: &WorkflowSnapshot) -> Result<Vec<u8>, BackendError> {
snapshot_format::encode_framed(&self.codec, snapshot)
.map_err(|e| BackendError::Serialization(e.to_string()))
}
pub(crate) fn encode_blob(
&self,
snapshot: &mut WorkflowSnapshot,
) -> Result<(Vec<u8>, [u8; 32]), BackendError> {
snapshot.strip_task_outputs();
let data = self.encode(snapshot)?;
let hash = crate::history::snapshot_hash(&data);
Ok((data, hash))
}
pub(crate) fn encode_blob_preserving(
&self,
snapshot: &mut WorkflowSnapshot,
) -> Result<(Vec<u8>, [u8; 32]), BackendError> {
let outputs = snapshot.take_task_outputs();
let encoded = self.encode(snapshot);
snapshot.hydrate_task_outputs(outputs);
let data = encoded?;
let hash = crate::history::snapshot_hash(&data);
Ok((data, hash))
}
}
impl<C> PostgresBackend<C>
where
C: Decoder + CodecIdentity + codec::sealed::DecodeValue<WorkflowSnapshot>,
{
pub(crate) fn decode(&self, data: &[u8]) -> Result<WorkflowSnapshot, BackendError> {
snapshot_format::decode_framed(&self.codec, data)
.map_err(|e| BackendError::Serialization(e.to_string()))
}
}
async fn check_pg_version(pool: &PgPool) -> Result<(), BackendError> {
let row = sqlx::query("SHOW server_version_num")
.fetch_one(pool)
.await
.map_err(PgError)?;
let version_str: &str = row.get("server_version_num");
let version_num: u32 = version_str.parse().map_err(|e| {
BackendError::Backend(format!(
"failed to parse server_version_num '{version_str}': {e}"
))
})?;
let major = version_num / 10000;
tracing::info!(pg_version = major, "connected to PostgreSQL {major}");
if major < MIN_PG_MAJOR_VERSION {
return Err(BackendError::Backend(format!(
"PostgreSQL {major} is not supported (minimum: {MIN_PG_MAJOR_VERSION})"
)));
}
Ok(())
}
fn duration_to_ms(d: Duration) -> i32 {
let ms = d.as_millis();
if ms == 0 {
1
} else {
i32::try_from(ms).unwrap_or(i32::MAX)
}
}