Skip to main content

PostgresOutboxStore

Struct PostgresOutboxStore 

Source
#[non_exhaustive]
pub struct PostgresOutboxStore<Ser = JsonSerializer> { /* private fields */ }
Expand description

Reliar’s PostgreSQL outbox provider. Cheap to clone into an AppState — it wraps a PgPool; no outer Arc required. The connection pool stays the host’s: Reliar never owns or reads a DATABASE_URL.

The default type parameter only exists behind the crate’s default json feature (contract §4, review 1 B2): under --no-default-features there is no default, so Self::connect is the only constructor and cargo hack --feature-powerset compiles every combination.

Implementations§

Source§

impl<Ser: Serializer + Send + Sync + 'static> PostgresOutboxStore<Ser>

Source

pub async fn connect( pool: PgPool, settings: PostgresOutboxSettings, serializer: Ser, ) -> Result<Self, PostgresStoreError>

Wraps pool with settings and serializer. Verifies once at construction that the unqualified name outbox resolves to settings.schema: fails fast with PostgresStoreError::SchemaResolution (search_path problem) or PostgresStoreError::NotMigrated (the relation is missing entirely) rather than surprising the first acquire. Logs a tracing::warn! when a same-named table also exists in another schema on the path.

§Errors

Returns PostgresStoreError::NotMigrated, PostgresStoreError::SchemaResolution, or PostgresStoreError::Database for a connection failure during verification.

Source

pub fn content_type(&self) -> &ContentType

The ContentType this store writes to every row — Serializer::content_type(). The only way a caller can predict the content_type of an envelope it will later acquire: enqueue writes this value, ignoring whatever envelope.metadata.delivery.content_type held (contract §4).

Source

pub async fn enqueue<T: Message>( &self, tx: &mut Transaction<'_, Postgres>, envelope: &Envelope<T>, ) -> Result<MessageId, EnqueueError<Ser::Error>>

Stages a message in the application’s own transaction — atomicity is visible in the signature. Plain INSERT, no ON CONFLICT: a reused MessageId aborts the caller’s transaction rather than silently losing a message. Returns the id it wrote, so the caller can use it as the next message’s causation_id in the same transaction.

§Errors

Returns EnqueueError::Serialize if the configured Serializer rejects the body, EnqueueError::Duplicate for a reused MessageId, or EnqueueError::Database for any other sqlx failure.

Source

pub async fn enqueue_with<T: Message>( &self, tx: &mut Transaction<'_, Postgres>, envelope: &Envelope<T>, options: EnqueueOptions<'_>, ) -> Result<MessageId, EnqueueError<Ser::Error>>

Same as Self::enqueue, with provider-side options (currently EnqueueOptions::ordering_key).

§Errors

Same as Self::enqueue.

Source§

impl PostgresOutboxStore<JsonSerializer>

Source

pub async fn new(pool: PgPool) -> Result<Self, PostgresStoreError>

Convenience over Self::connect, behind the crate’s default json feature.

§Errors

Same as Self::connect.

Source

pub async fn with_settings( pool: PgPool, settings: PostgresOutboxSettings, ) -> Result<Self, PostgresStoreError>

Convenience over Self::connect with explicit settings, behind the crate’s default json feature.

§Errors

Same as Self::connect.

Trait Implementations§

Source§

impl<Ser> Clone for PostgresOutboxStore<Ser>

Manual impl, never derived: a derived Clone would condition on Ser: Clone. The serializer is held as Arc<Ser> — stateless and cheap to share — so cloning the store never requires the serializer itself to be Clone.

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl<Ser> Debug for PostgresOutboxStore<Ser>

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<Ser: Serializer + Send + Sync + 'static> OutboxDeadLetters for PostgresOutboxStore<Ser>

Source§

async fn list_dead( &self, query: DeadQuery, ) -> Result<DeadLetterPage, Self::Error>

ORDER BY sequence ASC is normative (contract §3.4): after_sequence is a keyset cursor over sequence, the column ix_outbox_dead orders by; message_type/ tenant_id/dead_before are filters only, expressed as ($n::type IS NULL OR ...) so one static statement serves every combination. The cursor returned is the largest sequence scanned, poisoned rows included, so a poisoned tail cannot loop the caller forever.

Source§

async fn retry_dead(&self, refs: &[MessageRef]) -> Result<u64, Self::Error>

Returns dead rows to pending: clears the lease that already isn’t there, resets attempts to 0 (the only operation that does), keeps last_error for audit. Not worker-guarded — a dead row holds no lease (contract §3.4).

Source§

async fn purge_dead(&self, refs: &[MessageRef]) -> Result<u64, Self::Error>

Deletes dead rows by reference, regardless of PurgeRequest::dead_retention.

Source§

type Error = PostgresStoreError

A failure of the call.
Source§

impl<Ser: Serializer + Send + Sync + 'static> OutboxStore for PostgresOutboxStore<Ser>

Source§

async fn acquire( &self, request: AcquireRequest, ) -> Result<AcquiredBatch, Self::Error>

The canonical single-statement claim (SRS §24.1, ADR 0006): a CTE SELECT … FOR UPDATE SKIP LOCKED feeding an UPDATE … RETURNING, so the row lock is released before this future resolves and no network I/O to a publisher can ever happen while it is held.

A row this call cannot decode is excluded from records, reported in poisoned, and moved to dead with DeadReason::Undecodable by a follow-up statement guarded by locked_by — the batch continues (§19.5, ADR 0008).

Source§

async fn complete( &self, worker: &WorkerId, items: &[CompletedMessage], ) -> Result<u64, Self::Error>

Marks rows published, worker-guarded (locked_by = $2). A row already completed or reclaimed by another worker contributes nothing to the count — a shortfall is logged at debug, never an error (ADR 0008).

Source§

async fn fail( &self, worker: &WorkerId, items: &[FailedMessage], ) -> Result<u64, Self::Error>

Applies each item’s FailureOutcome, worker-guarded. Retry rows get available_at = now() + delay computed in SQL (ADR 0009); dead rows get dead_at/ dead_reason set together (ck_outbox_dead_reason). Both increment attempts — on outcome, never on claim.

Source§

async fn release( &self, worker: &WorkerId, items: &[MessageRef], ) -> Result<u64, Self::Error>

Clears the lease for rows this worker still owns. available_at and attempts are untouched — a release is not a failure (SRS §26.1).

Source§

async fn extend_lease( &self, worker: &WorkerId, items: &[MessageRef], lease: Duration, ) -> Result<u64, Self::Error>

Renews locked_until = now() + lease for rows this worker still owns. Best-effort: a shortfall means the lease already expired (§21.1).

Source§

async fn purge(&self, request: PurgeRequest) -> Result<PurgeReport, Self::Error>

One bounded pass, three statements, each capped at request.batch_size (contract §7 G1): published-row delete, dead-row delete, and the expired→dead sweep — none of the three is ever an unbounded DELETE/UPDATE. The sweep’s predicate carries the claim’s lease clause (locked_until IS NULL OR locked_until < now()), so it never transitions a row a live worker still owns (contract §7 G2) — that worker’s own complete/fail wins, and the row becomes sweepable only once its lease lapses.

Source§

async fn stats(&self) -> Result<OutboxStats, Self::Error>

One statement, four FILTER-qualified aggregates over a single scan of outbox (contract §4, S8 EXPLAIN comparison, RELIAR-17 card Log). An earlier version issued four separate statements, one per ix_outbox_pending/ix_outbox_dead_at/ix_outbox_expires — but pending’s and the min(available_at) row’s predicates aren’t a strict subset of ix_outbox_pending (they also filter on available_at/locked_until/expires_at, none of which the partial index’s WHERE clause covers), so the planner chose a Seq Scan for both anyway on a realistic seeded table (20k rows, a 25/25/25/25 pending/dead/ published/expired-pending mix) — meaning the four-statement form paid for that same Seq Scan twice (once for pending, once for the min/now() row) plus three extra round trips, for a strictly worse total (Execution Time 4.65 ms combined, Buffers: shared hit 836) than one statement computing all four aggregates from one scan (Execution Time 2.77 ms, Buffers: shared hit 412) — see the card Log for both full EXPLAIN (ANALYZE, BUFFERS) plans.

Source§

type Error = PostgresStoreError

A failure of the call — never a property of one row’s content. Must self-classify via crate::Classify so run() (S4) can tell a transient outage from a permanent one (ADR 0014).

Auto Trait Implementations§

§

impl<Ser = JsonSerializer> !RefUnwindSafe for PostgresOutboxStore<Ser>

§

impl<Ser = JsonSerializer> !UnwindSafe for PostgresOutboxStore<Ser>

§

impl<Ser> Freeze for PostgresOutboxStore<Ser>
where Arc<Ser>: Freeze,

§

impl<Ser> Send for PostgresOutboxStore<Ser>
where Arc<Ser>: Send,

§

impl<Ser> Sync for PostgresOutboxStore<Ser>
where Arc<Ser>: Sync,

§

impl<Ser> Unpin for PostgresOutboxStore<Ser>
where Arc<Ser>: Unpin,

§

impl<Ser> UnsafeUnpin for PostgresOutboxStore<Ser>
where Arc<Ser>: UnsafeUnpin,

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more