Skip to main content

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}