use reliar_core::MessageId;
use reliar_inbox::InboxStore;
use reliar_inbox::{
InboxClaim, InboxFailure, InboxMessage, InboxPurgeReport, InboxPurgeRequest, InboxRecord,
InboxScope,
};
use sqlx::{PgPool, Postgres, Transaction};
use tracing::Instrument as _;
use crate::connection::schema;
use crate::settings::PostgresInboxSettings;
use super::error::PostgresInboxError;
use super::{claim, outcomes, purge};
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct PostgresInboxStore {
pub(super) pool: PgPool,
pub(super) settings: PostgresInboxSettings,
}
impl PostgresInboxStore {
pub async fn connect(
pool: PgPool,
settings: PostgresInboxSettings,
) -> Result<Self, PostgresInboxError> {
settings.validate()?;
let detected = crate::connection::version::detected_server_version_num(&pool).await?;
if detected < crate::MIN_SERVER_VERSION_NUM {
return Err(PostgresInboxError::UnsupportedServerVersion {
required: crate::MIN_SERVER_VERSION_NUM,
detected,
});
}
let check = schema::verify_table_schema(&pool, &settings.schema, "inbox", &[]).await?;
let resolved_here = check.resolved_schema.as_deref() == Some(settings.schema.as_str());
if !resolved_here {
if !check.configured_exists {
return Err(PostgresInboxError::NotMigrated {
schema: settings.schema,
});
}
return Err(PostgresInboxError::SchemaNotOnSearchPath {
configured: settings.schema,
observed: check.search_path,
});
}
let others = schema::other_table_schemas(&pool, &settings.schema, "inbox").await?;
if !others.is_empty() {
tracing::warn!(
configured_schema = %settings.schema,
other_schemas = ?others,
"a table named `inbox` also exists outside the configured schema; an \
unqualified reference from another session could resolve to it"
);
}
Ok(Self { pool, settings })
}
pub(super) async fn set_local_timeout(
&self,
tx: &mut Transaction<'_, Postgres>,
) -> Result<(), PostgresInboxError> {
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(())
}
}
impl<'c> InboxStore<Transaction<'c, Postgres>> for PostgresInboxStore {
type Error = PostgresInboxError;
fn claim(
&self,
tx: &mut Transaction<'c, Postgres>,
scope: &InboxScope,
message: InboxMessage<'_>,
) -> impl Future<Output = Result<InboxClaim, Self::Error>> + Send {
let span = tracing::debug_span!(
"reliar.inbox.claim",
inbox.scope = %scope,
message.id = %message.id,
message.r#type = %message.message_type,
inbox.outcome = tracing::field::Empty,
inbox.attempt = tracing::field::Empty,
inbox.attempts = tracing::field::Empty,
inbox.record_id = tracing::field::Empty,
);
let recording_span = span.clone();
async move {
let result = claim::claim(self, tx, scope, message).await;
if let Ok(claim) = &result {
record_claim_outcome(&recording_span, claim);
}
result
}
.instrument(span)
}
fn complete(
&self,
tx: &mut Transaction<'c, Postgres>,
scope: &InboxScope,
id: MessageId,
) -> impl Future<Output = Result<(), Self::Error>> + Send {
let span = tracing::debug_span!(
"reliar.inbox.complete",
inbox.scope = %scope,
message.id = %id,
);
async move { outcomes::complete(self, tx, scope, id).await }.instrument(span)
}
fn fail(
&self,
scope: &InboxScope,
message: InboxMessage<'_>,
error: &(dyn std::error::Error + 'static),
) -> impl Future<Output = Result<InboxFailure, Self::Error>> + Send {
let last_error = outcomes::format_error_chain(error);
let span = tracing::debug_span!(
"reliar.inbox.fail",
inbox.scope = %scope,
message.id = %message.id,
message.r#type = %message.message_type,
inbox.outcome = tracing::field::Empty,
inbox.attempts = tracing::field::Empty,
inbox.record_id = tracing::field::Empty,
);
let recording_span = span.clone();
async move {
let result = outcomes::fail(self, scope, message, last_error).await;
if let Ok(failure) = &result {
record_fail_outcome(&recording_span, failure);
}
result
}
.instrument(span)
}
async fn find(
&self,
scope: &InboxScope,
id: MessageId,
) -> Result<Option<InboxRecord>, Self::Error> {
purge::find(self, scope, id).await
}
fn purge(
&self,
request: InboxPurgeRequest,
) -> impl Future<Output = Result<InboxPurgeReport, Self::Error>> + Send {
let span = tracing::debug_span!(
"reliar.inbox.purge",
inbox.completed_deleted = tracing::field::Empty,
inbox.incomplete_deleted = tracing::field::Empty,
inbox.dead_deleted = tracing::field::Empty,
);
let recording_span = span.clone();
async move {
let result = purge::purge(self, request).await;
if let Ok(report) = &result {
recording_span.record("inbox.completed_deleted", report.completed_deleted);
recording_span.record("inbox.incomplete_deleted", report.incomplete_deleted);
recording_span.record("inbox.dead_deleted", report.dead_deleted);
}
result
}
.instrument(span)
}
}
fn record_claim_outcome(span: &tracing::Span, claim: &InboxClaim) {
match claim {
InboxClaim::Claimed { attempt } => {
span.record("inbox.outcome", "claimed");
span.record("inbox.attempt", attempt);
}
InboxClaim::AlreadyCompleted { .. } => {
span.record("inbox.outcome", "already_completed");
}
InboxClaim::InProgress => {
span.record("inbox.outcome", "in_progress");
}
InboxClaim::Dead { id, attempts, .. } => {
span.record("inbox.outcome", "dead");
span.record("inbox.record_id", tracing::field::display(id));
span.record("inbox.attempts", attempts);
}
_ => {}
}
}
fn record_fail_outcome(span: &tracing::Span, failure: &InboxFailure) {
match failure {
InboxFailure::Recorded { attempts } => {
span.record("inbox.outcome", "recorded");
span.record("inbox.attempts", attempts);
}
InboxFailure::Dead {
id,
attempts,
dead_at: _,
} => {
span.record("inbox.outcome", "dead");
span.record("inbox.attempts", attempts);
span.record("inbox.record_id", tracing::field::display(id));
}
InboxFailure::AlreadyCompleted => {
span.record("inbox.outcome", "already_completed");
}
_ => {}
}
}