ruststream_sqlx/inbox/transactional/tx.rs
1//! The transaction a transactional delivery lends its handler, and where the lender finds it: in a
2//! book of transactions, or on the session of an advisory lock.
3
4use std::fmt;
5use std::ops::{Deref, DerefMut};
6use std::thread;
7
8use sqlx::Database;
9
10use super::book::{TxBook, TxSlot};
11use crate::inbox::database::QueueDatabase;
12use crate::inbox::form::advisory::{LockBook, LockSlot, Locked};
13use crate::inbox::tx::PoolTx;
14
15/// The delivery's transaction, lent to the handler: it dereferences to the connection the
16/// transaction runs on, and goes back to the delivery when the handler ends.
17///
18/// A handler takes it as its first `Ctx` parameter, `Ctx(mut tx): Ctx<keys::Tx<DB>>`, on a
19/// registration mounted with [`transactional`](super::InboxSettings::transactional). It writes
20/// through `&mut *tx`, as through the connection of a sqlx transaction, and `(&mut *tx).begin()`
21/// nests a savepoint of its own. The delivery's settlement ends the transaction: acknowledgement
22/// commits it, every other outcome rolls the handler's writes back first. Dropping `Tx` gives the
23/// transaction back and ends nothing. A `Tx` dropped by a panic marks the handler's writes for
24/// the rollback, so a delivery acknowledged after its handler panicked, as the `skip` panic policy
25/// does, commits none of them.
26///
27/// `Tx` is an owned value, so a handler may move it into a task of its own. The transaction must
28/// be back when the delivery settles: a settlement that finds it still lent fails with
29/// [`SqlxBrokerError::TransactionHeld`](crate::SqlxBrokerError::TransactionHeld), and the
30/// transaction rolls back when that `Tx` drops.
31///
32/// # Examples
33///
34/// ```
35/// # #[cfg(feature = "postgres")]
36/// # mod demo {
37/// use ruststream_sqlx::prelude::*;
38/// use sqlx::{PgPool, Postgres};
39///
40/// #[derive(Inbox, sqlx::FromRow)]
41/// #[inbox(table = "payment_jobs")]
42/// pub struct PaymentJob {
43/// #[field(id)]
44/// id: i64,
45/// #[field(payload)]
46/// payload: Vec<u8>,
47/// }
48///
49/// #[derive(serde::Deserialize)]
50/// struct Payment {
51/// account: String,
52/// cents: i64,
53/// }
54///
55/// #[subscriber(InboxQueue::<PaymentJob>::new("payments"))]
56/// async fn post(payment: &Payment, Ctx(mut tx): Ctx<keys::Tx<Postgres>>) -> HandlerOutcome {
57/// let posted = sqlx::query("INSERT INTO ledger (account, cents) VALUES ($1, $2)")
58/// .bind(&payment.account)
59/// .bind(payment.cents)
60/// .execute(&mut *tx)
61/// .await;
62/// match posted {
63/// // The commit keeps the ledger row and finishes the job, together.
64/// Ok(_) => HandlerOutcome::ack(),
65/// // The retry discards the ledger row and returns the job to the queue.
66/// Err(error) => {
67/// tracing::warn!(%error, "the payment was not posted");
68/// HandlerOutcome::retry()
69/// }
70/// }
71/// }
72///
73/// pub fn app(pool: PgPool) -> RustStream {
74/// RustStream::new(AppInfo::new("payments", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
75/// b.include(post.transactional());
76/// })
77/// }
78/// # }
79/// # fn main() {}
80/// ```
81pub struct Tx<DB: QueueDatabase> {
82 /// The transaction, until `Tx` drops and gives it back.
83 lent: Option<Lent<DB>>,
84}
85
86impl<DB: QueueDatabase> Tx<DB> {
87 /// The transaction `lender` lends, out of its book until the handler ends; `None` when the book
88 /// does not hold it for the delivery, as once it is lent.
89 pub(crate) fn lent_by(lender: Lender<DB>) -> Option<Self> {
90 Some(Self {
91 lent: Some(lender.lend()?),
92 })
93 }
94
95 fn lent(&self) -> &Lent<DB> {
96 self.lent
97 .as_ref()
98 .expect("a `Tx` holds its transaction until it drops")
99 }
100
101 fn lent_mut(&mut self) -> &mut Lent<DB> {
102 self.lent
103 .as_mut()
104 .expect("a `Tx` holds its transaction until it drops")
105 }
106}
107
108impl<DB: QueueDatabase> Deref for Tx<DB> {
109 type Target = DB::Connection;
110
111 /// The connection inside the delivery's transaction.
112 ///
113 /// # Panics
114 ///
115 /// Never: `Tx` holds the transaction until it drops.
116 fn deref(&self) -> &DB::Connection {
117 self.lent().conn()
118 }
119}
120
121impl<DB: QueueDatabase> DerefMut for Tx<DB> {
122 /// The connection inside the delivery's transaction, to run a statement on.
123 ///
124 /// # Panics
125 ///
126 /// Never: `Tx` holds the transaction until it drops.
127 fn deref_mut(&mut self) -> &mut DB::Connection {
128 self.lent_mut().conn_mut()
129 }
130}
131
132impl<DB: QueueDatabase> AsMut<DB::Connection> for Tx<DB> {
133 fn as_mut(&mut self) -> &mut DB::Connection {
134 self
135 }
136}
137
138impl<DB: QueueDatabase> Drop for Tx<DB> {
139 fn drop(&mut self) {
140 if let Some(lent) = self.lent.take() {
141 // A handler that panicked half way through its writes gives them back marked, so no
142 // outcome commits them.
143 lent.give_back(thread::panicking());
144 }
145 }
146}
147
148impl<DB: QueueDatabase> fmt::Debug for Tx<DB> {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 f.debug_struct("Tx").finish_non_exhaustive()
151 }
152}
153
154/// What a delivery lends its handler: the transaction it settles in, with the slot it goes back
155/// to.
156pub(crate) enum Lent<DB: Database> {
157 /// A transaction of the crate's own on a pool connection, from its slot in a [`TxBook`]: the
158 /// claim's in the row lock form, the delivery's own in the lease form.
159 Tx {
160 book: &'static TxBook<DB>,
161 slot: TxSlot,
162 tx: PoolTx<DB>,
163 },
164 /// The session that holds the lock on the row's key, its transaction open, from its slot in a
165 /// [`LockBook`]: the advisory lock form.
166 Session {
167 book: &'static LockBook<DB>,
168 slot: LockSlot,
169 locked: Locked<'static, DB>,
170 },
171}
172
173impl<DB: Database> Lent<DB> {
174 fn conn(&self) -> &DB::Connection {
175 match self {
176 Self::Tx { tx, .. } => tx,
177 Self::Session { locked, .. } => locked.conn_ref(),
178 }
179 }
180
181 fn conn_mut(&mut self) -> &mut DB::Connection {
182 match self {
183 Self::Tx { tx, .. } => tx,
184 Self::Session { locked, .. } => locked.conn(),
185 }
186 }
187
188 /// Back into the delivery's slot after the handler, `panicked` when a panic dropped it: one
189 /// lock. A slot that no longer waits for it, its delivery settled or dropped meanwhile, leaves
190 /// it to end: a transaction's connection closes and the server rolls it back; a session closes
191 /// after the unlock of its key, which ends its transaction too.
192 pub(super) fn give_back(self, panicked: bool) {
193 match self {
194 Self::Tx { book, slot, tx } => book.give_back(slot, tx, panicked),
195 Self::Session { book, slot, locked } => book.give_back(slot, locked, panicked),
196 }
197 }
198}
199
200/// Where a delivery's handler borrows the delivery's transaction from: its slot in the
201/// subscription's book. Copied into the handler's context: a reference, the slot's index and its
202/// generation.
203pub(crate) enum Lender<DB: Database> {
204 /// The delivery's slot in a [`TxBook`].
205 Tx(&'static TxBook<DB>, TxSlot),
206 /// The delivery's slot in a [`LockBook`], whose session holds the transaction.
207 Lock(&'static LockBook<DB>, LockSlot),
208}
209
210impl<DB: Database> Clone for Lender<DB> {
211 fn clone(&self) -> Self {
212 *self
213 }
214}
215
216impl<DB: Database> Copy for Lender<DB> {}
217
218impl<DB: Database> fmt::Debug for Lender<DB> {
219 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220 match self {
221 Self::Tx(_, slot) => f.debug_tuple("Tx").field(slot).finish(),
222 Self::Lock(_, slot) => f.debug_tuple("Lock").field(slot).finish(),
223 }
224 }
225}
226
227impl<DB: Database> Lender<DB> {
228 /// The delivery's transaction, out of its book for the handler: one lock. `None` when the book
229 /// does not hold it for this delivery, as once it is lent.
230 pub(crate) fn lend(self) -> Option<Lent<DB>> {
231 match self {
232 Self::Tx(book, slot) => book.lend(slot).map(|tx| Lent::Tx { book, slot, tx }),
233 Self::Lock(book, slot) => {
234 book.lend(slot)
235 .map(|locked| Lent::Session { book, slot, locked })
236 }
237 }
238 }
239}