use reliar_core::MessageId;
use reliar_inbox::{InboxFailure, InboxMessage, InboxRecordId, InboxScope};
use sqlx::{Postgres, Transaction};
use super::error::PostgresInboxError;
use crate::connection::schema::{restore_search_path, set_search_path};
use crate::records::truncate_last_error;
use super::PostgresInboxStore;
pub(super) async fn complete(
store: &PostgresInboxStore,
tx: &mut Transaction<'_, Postgres>,
scope: &InboxScope,
message_id: MessageId,
) -> Result<(), PostgresInboxError> {
let restore = if store.settings.claim_sets_search_path {
Some(set_search_path(tx, &store.settings.schema).await?)
} else {
None
};
let result = complete_locked(tx, scope, message_id).await;
if result.is_ok()
&& let Some(previous) = restore
{
restore_search_path(tx, &previous).await?;
}
result
}
async fn complete_locked(
tx: &mut Transaction<'_, Postgres>,
scope: &InboxScope,
message_id: MessageId,
) -> Result<(), PostgresInboxError> {
let scope = scope.as_str();
let id = message_id.as_uuid();
let result = sqlx::query!(
r#"UPDATE inbox SET completed_at = now(), updated_at = now()
WHERE scope = $1 AND message_id = $2 AND completed_at IS NULL AND dead_at IS NULL"#,
scope,
id,
)
.execute(&mut **tx)
.await?;
if result.rows_affected() == 0 {
return Err(PostgresInboxError::NotClaimed {
scope: scope.to_owned(),
message_id,
});
}
Ok(())
}
pub(super) async fn fail(
store: &PostgresInboxStore,
scope: &InboxScope,
message: InboxMessage<'_>,
last_error: String,
) -> Result<InboxFailure, PostgresInboxError> {
let scope_str = scope.as_str();
let last_error = truncate_last_error(last_error);
let max_attempts = i32::try_from(store.settings.max_attempts).unwrap_or(i32::MAX);
let row = if store.settings.statement_timeout.is_zero() {
fail_row(&store.pool, scope_str, message, &last_error, max_attempts).await?
} else {
let mut tx = store.pool.begin().await?;
store.set_local_timeout(&mut tx).await?;
let row = fail_row(&mut *tx, scope_str, message, &last_error, max_attempts).await?;
tx.commit().await?;
row
};
let Some(row) = row else {
return Ok(InboxFailure::AlreadyCompleted);
};
let attempts = u32::try_from(row.attempts).unwrap_or(u32::MAX);
Ok(match row.dead_at {
Some(dead_at) => InboxFailure::Dead {
id: InboxRecordId::from_uuid(row.id),
attempts,
dead_at,
},
None => InboxFailure::Recorded { attempts },
})
}
struct FailedRow {
id: uuid::Uuid,
attempts: i32,
dead_at: Option<time::OffsetDateTime>,
}
async fn fail_row<'e>(
executor: impl sqlx::PgExecutor<'e>,
scope: &str,
message: InboxMessage<'_>,
last_error: &str,
max_attempts: i32,
) -> Result<Option<FailedRow>, sqlx::Error> {
let id = InboxRecordId::new().as_uuid();
let message_id = message.id.as_uuid();
let message_type = message.message_type.name();
let message_version = i32::from(message.message_type.version());
let conversation_id = message.conversation_id.as_uuid();
let correlation_id = message
.correlation_id
.map(reliar_core::CorrelationId::as_str);
let causation_id = message.causation_id.map(|c| c.as_uuid());
sqlx::query_as!(
FailedRow,
r#"INSERT INTO inbox (id, scope, message_id, message_type, message_version,
conversation_id, correlation_id, causation_id,
attempts, last_error, dead_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8,
1, $9, CASE WHEN $10::integer <= 1 THEN now() END)
ON CONFLICT (scope, message_id) DO UPDATE
SET attempts = inbox.attempts + 1,
last_error = excluded.last_error,
dead_at = COALESCE(inbox.dead_at,
CASE WHEN inbox.attempts + 1 >= $10::integer THEN now() END),
updated_at = now()
WHERE inbox.completed_at IS NULL
RETURNING id, attempts, dead_at"#,
id,
scope,
message_id,
message_type,
message_version,
conversation_id,
correlation_id,
causation_id,
last_error,
max_attempts,
)
.fetch_optional(executor)
.await
}
pub(super) fn format_error_chain(error: &(dyn std::error::Error + 'static)) -> String {
let mut out = error.to_string();
let mut source = error.source();
while let Some(err) = source {
out.push_str(": ");
out.push_str(&err.to_string());
source = err.source();
}
out
}