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