1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
//! `reliar-outbox` is the storage-agnostic transactional outbox: the [`OutboxStore`]/
//! [`OutboxDeadLetters`] capability traits (plus `reliar_core::Publisher`, re-exported here for
//! convenience), the request and result types that cross their boundary, a pure [`RetryPolicy`],
//! the feature's [`OutboxSettings`], and the [`OutboxMetrics`] hook.
//!
//! **The object names the guarantee — there is no facade type joining the two** (ADR 0036
//! amendment B). A caller enqueues durably by calling [`OutboxEnqueue::enqueue`] on the
//! provider store, in its own transaction: the message becomes visible when that transaction
//! commits and is published later by an [`OutboxDispatcher`], at-least-once, with the duplicate
//! windows below. A caller that instead wants to send now, with no Reliar durability at all, calls
//! the transport's own [`Publisher::publish`] directly — `reliar-outbox` never wraps it. Nothing
//! decides between the two at runtime, and no setting can.
//!
//! This slice ships the traits and types a provider builds against, a host configures, and the
//! [`OutboxDispatcher`] worker loop. Store behaviour is proven only against real Postgres (ADR
//! 0043): this crate constructs no store of its own, so the example below is a compiled generic
//! call shape, never invoked — `reliar-store-postgres`'s own rustdoc has the runnable version
//! over `PgPool::connect`.
//!
//! ```
//! use reliar_core::{Message, Publisher, SerializedEnvelope};
//! use reliar_outbox::OutboxEnqueue;
//!
//! # #[derive(serde::Serialize, serde::Deserialize)]
//! # struct OrderCreated;
//! # impl Message for OrderCreated {
//! # const TYPE: &'static str = "orders.created";
//! # const VERSION: u16 = 1;
//! # }
//! #
//! /// The durable path: any provider store implementing `OutboxEnqueue<Tx>` enqueues in the
//! /// caller's own transaction — no facade type in between (ADR 0036 amendment B). A bare
//! /// message becomes an envelope with default metadata and a freshly rooted conversation;
//! /// published later by an `OutboxDispatcher`. Use `enqueue_envelope` with `Envelope::builder(..)`
//! /// instead when an id must propagate from an inbound request.
//! async fn enqueue_order<S, Tx>(store: &S, tx: &mut Tx, body: OrderCreated) -> Result<(), S::Error>
//! where
//! S: OutboxEnqueue<Tx>,
//! {
//! store.enqueue(tx, body).await?;
//! Ok(())
//! }
//!
//! /// The bypass path needs no store: any `reliar_core::Publisher` — a real transport, or a
//! /// harness-local stand-in — sends now, with none of the outbox's guarantees.
//! async fn publish_now<P: Publisher>(
//! publisher: &P,
//! envelope: &SerializedEnvelope,
//! ) -> Result<(), P::Error> {
//! publisher.publish(envelope).await
//! }
//! ```
//!
//! # Guarantees
//!
//! - **Durable at-least-once publication. Never exactly-once.** Duplicate delivery is expected
//! and must be handled by an idempotent consumer. Three distinct windows produce a duplicate,
//! and all three are unavoidable:
//! 1. **The crash window:** a publish reaches the broker, the worker crashes before
//! `complete` persists, the lease expires, and another worker republishes the same message.
//! 2. **The slow-batch window:** no crash at all — a worker claims a large batch under
//! a lease shorter than the batch takes to drain, the lease expires while the worker is
//! still healthily publishing, a second worker reclaims and republishes the tail, and the
//! first worker's later `complete`/`fail` is fenced out by the row's `claim_token` (ADR 0046 A) —
//! it affects zero rows.
//! 3. **The drain window:** on cancellation, `run()` drains in-flight publishes for at
//! most `DispatcherSettings::drain_timeout`; a publish still unresolved at the timeout is
//! released rather than awaited further, and its outcome — success or failure — is the same
//! duplicate risk as the other two windows, just triggered by shutdown instead of a lease.
//! - **Ordering is a property of the message, not a setting.** A message type that declares
//! [`reliar_core::Message::order_by`] is published one at a time, in enqueue (`id`) order,
//! across **all** workers, with no dispatcher or store configuration (ADR 0051): among the rows
//! of a key visible to a claim, only the earliest one that could still be published is
//! claimable, so the claim never returns more than one row of a given
//! [`reliar_core::OrderingKey`] at a time. A message whose `order_by` returns `None` — the
//! default — carries no ordering guarantee of any kind: `SKIP LOCKED`, concurrent publishing,
//! per-message backoff and multiple workers reorder it freely.
//!
//! **The order is total only when the host serialises each key's writes** (ADR 0051 Amendment
//! B). Take the aggregate's own row lock *before* enqueuing — load and lock the aggregate,
//! mutate it, enqueue, commit — and the next writer of that key blocks until the first commits,
//! so commit order is `id` order; one transaction per sequence step does the same. Without that,
//! a row that becomes visible later with a lower `id` (an insert committing after a higher-`id`
//! row of the key was already claimed), or a dead row revived by `OutboxDeadLetters::retry_dead`
//! under a live successor, becomes a new head and may be published while a later row of the key
//! is still in flight. Per-key ordering also never spans keys, never survives a lease expiry
//! mid-publish (the crash window reorders exactly as it duplicates), and makes no promise about
//! the broker or its consumers.
//! - **Pure retry.** [`RetryPolicy`] is I/O-free and clock-free: it returns a [`core::time::Duration`],
//! never a timestamp. The store applies it as `available_at = now() + delay` in SQL, so a
//! worker's clock skew can never hot-loop a row or park it in the future (ADR 0009).
//! - **The library never reads the environment implicitly.** Only [`OutboxSettings::from_env`]
//! touches `std::env`, and only when called (ADR 0019).
//! - **Calling the transport publisher directly bypasses the outbox.** A call to a transport's
//! [`Publisher::publish`] carries **none** of the above: one attempt, no retry, no backoff, no
//! dead state, no duplicate window — only as much retry as the transport itself performs, and
//! no relationship to any transaction the caller has open. Use [`OutboxEnqueue::enqueue`]/
//! [`OutboxEnqueue::enqueue_envelope`] for the durable path instead (ADR 0036 amendment B).
//! See `docs/guides/outbox-enqueue-and-publish.md` for the full comparison.
pub use ClaimToken;
pub use ;
pub use OutboxEnqueue;
pub use ConfigError;
pub use ;
pub use ;
pub use OutboxRecordId;
/// Re-exported from `reliar-core` (ADR 0032): a store author's or a publisher's `Classify`
/// bound, a publish/store failure's `FailureKind`, the `Publisher` capability trait, and the
/// shared `SettingsError` all live in core now. New code should name `reliar_core::` directly;
/// this re-export keeps existing `use reliar_outbox::{…}` imports one line.
pub use ;
pub use ;
pub use ;
pub use ;
pub use WorkerId;