use std::sync::Arc;
use reliar_core::{ContentType, Serializer};
use reliar_outbox::{
AcquireRequest, AcquiredBatch, CompletedRecord, FailedRecord, OutboxStats, OutboxStore,
PurgeReport, PurgeRequest, RecordRef, WorkerId,
};
use sqlx::{PgPool, Postgres, Transaction};
use crate::connection::schema;
use crate::settings::PostgresOutboxSettings;
#[cfg(feature = "json")]
use reliar_core::JsonSerializer;
use super::error::{self, PostgresOutboxError};
use super::{claim, outcomes, purge};
const REQUIRED_OUTBOX_COLUMNS: &[&str] = &["message_id", "id"];
#[cfg_attr(not(feature = "json"), doc = "```ignore")]
#[cfg_attr(feature = "json", doc = "```no_run")]
#[non_exhaustive]
pub struct PostgresOutboxStore<
#[cfg(feature = "json")] Ser = JsonSerializer,
#[cfg(not(feature = "json"))] Ser,
> {
pub(super) pool: PgPool,
pub(super) settings: PostgresOutboxSettings,
pub(super) serializer: Arc<Ser>,
}
impl<Ser> Clone for PostgresOutboxStore<Ser> {
fn clone(&self) -> Self {
Self {
pool: self.pool.clone(),
settings: self.settings.clone(),
serializer: Arc::clone(&self.serializer),
}
}
}
impl<Ser> std::fmt::Debug for PostgresOutboxStore<Ser> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PostgresOutboxStore")
.field("settings", &self.settings)
.finish_non_exhaustive()
}
}
impl<Ser: Serializer + Send + Sync + 'static> PostgresOutboxStore<Ser> {
pub async fn connect(
pool: PgPool,
settings: PostgresOutboxSettings,
serializer: Ser,
) -> Result<Self, PostgresOutboxError> {
if !schema::is_valid_schema_name(&settings.schema) {
return Err(PostgresOutboxError::InvalidSchema {
schema: settings.schema,
});
}
let detected = crate::connection::version::detected_server_version_num(&pool).await?;
if detected < crate::MIN_SERVER_VERSION_NUM {
return Err(PostgresOutboxError::UnsupportedServerVersion {
required: crate::MIN_SERVER_VERSION_NUM,
detected,
});
}
let check =
schema::verify_table_schema(&pool, &settings.schema, "outbox", REQUIRED_OUTBOX_COLUMNS)
.await
.map_err(|err| error::map_operational_error(&settings.schema, err))?;
let resolved_here = check.resolved_schema.as_deref() == Some(settings.schema.as_str());
if !resolved_here {
if !check.configured_exists {
return Err(PostgresOutboxError::NotMigrated {
schema: settings.schema,
});
}
return Err(PostgresOutboxError::SchemaNotOnSearchPath {
configured: settings.schema,
observed: check.search_path,
});
}
if let Some(&missing) = REQUIRED_OUTBOX_COLUMNS
.iter()
.find(|col| !check.satisfied_required_columns.iter().any(|c| c == *col))
{
return Err(PostgresOutboxError::SchemaOutOfDate {
schema: settings.schema,
missing,
});
}
let others = schema::other_table_schemas(&pool, &settings.schema, "outbox")
.await
.map_err(PostgresOutboxError::from)?;
if !others.is_empty() {
tracing::warn!(
configured_schema = %settings.schema,
other_schemas = ?others,
"a table named `outbox` also exists outside the configured schema; \
an unqualified reference from another session could resolve to it"
);
}
Ok(Self {
pool,
settings,
serializer: Arc::new(serializer),
})
}
#[cfg_attr(not(feature = "json"), doc = "```ignore")]
#[cfg_attr(feature = "json", doc = "```no_run")]
#[must_use]
pub fn content_type(&self) -> &ContentType {
self.serializer.content_type()
}
pub(super) fn map_err(&self, err: sqlx::Error) -> PostgresOutboxError {
error::map_operational_error(&self.settings.schema, err)
}
pub(super) async fn set_local_timeout(
&self,
tx: &mut Transaction<'_, Postgres>,
) -> Result<(), PostgresOutboxError> {
self.set_local_timeout_raw(tx)
.await
.map_err(|e| self.map_err(e))
}
pub(super) async fn set_local_timeout_raw(
&self,
tx: &mut Transaction<'_, Postgres>,
) -> Result<(), sqlx::Error> {
let timeout_ms = i64::try_from(self.settings.statement_timeout.as_millis())
.unwrap_or(i64::MAX)
.to_string();
sqlx::query_scalar!(
"SELECT set_config('statement_timeout', $1, true)",
timeout_ms
)
.fetch_one(&mut **tx)
.await?;
Ok(())
}
}
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
impl PostgresOutboxStore<JsonSerializer> {
pub async fn new(pool: PgPool) -> Result<Self, PostgresOutboxError> {
Self::connect(pool, PostgresOutboxSettings::default(), JsonSerializer).await
}
pub async fn with_settings(
pool: PgPool,
settings: PostgresOutboxSettings,
) -> Result<Self, PostgresOutboxError> {
Self::connect(pool, settings, JsonSerializer).await
}
}
impl<Ser: Serializer + Send + Sync + 'static> OutboxStore for PostgresOutboxStore<Ser> {
type Error = PostgresOutboxError;
async fn acquire(&self, request: AcquireRequest) -> Result<AcquiredBatch, Self::Error> {
claim::acquire(self, request).await
}
async fn complete(
&self,
worker: &WorkerId,
items: &[CompletedRecord],
) -> Result<u64, Self::Error> {
outcomes::complete(self, worker, items).await
}
async fn fail(&self, worker: &WorkerId, items: &[FailedRecord]) -> Result<u64, Self::Error> {
outcomes::fail(self, worker, items).await
}
async fn release(&self, worker: &WorkerId, items: &[RecordRef]) -> Result<u64, Self::Error> {
outcomes::release(self, worker, items).await
}
async fn extend_lease(
&self,
worker: &WorkerId,
items: &[RecordRef],
lease: std::time::Duration,
) -> Result<u64, Self::Error> {
outcomes::extend_lease(self, worker, items, lease).await
}
async fn purge(&self, request: PurgeRequest) -> Result<PurgeReport, Self::Error> {
purge::purge(self, request).await
}
async fn stats(&self) -> Result<OutboxStats, Self::Error> {
purge::stats(self).await
}
}