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}