ruststream_sqlx/inbox/keys.rs
1//! What a handler reads off the delivery it handles, through `Ctx<Key>`.
2//!
3//! A handler's first `Ctx` names its context: [`Tx`] reads [`TxContext`], which a transactional
4//! delivery lends; [`Pool`] reads [`PoolContext`], which every delivery of an `InboxQueue`
5//! subscription lends; [`Attempt`] reads [`InboxContext`], which any delivery lends. A key after
6//! the first reads any context that carries it, so a handler takes them in the order `Tx`, then
7//! `Pool`, then `Attempt`, and leaves out the ones it does not need.
8
9use std::convert::Infallible;
10use std::fmt;
11use std::future::Future;
12use std::marker::PhantomData;
13
14use ruststream::runtime::{Context, Ctx, FromContext};
15use ruststream::{BuildContext, ContextField, Field, IncomingMessage};
16use sqlx::Pool as DbPool;
17
18use super::database::QueueDatabase;
19use super::delivery::InboxDelivery;
20use super::engine::Events;
21use super::pools::ServicePool;
22use super::transactional::{InboxMode, Lender};
23
24/// A delivery's context: what the inbox lends a handler besides the message.
25///
26/// # Examples
27///
28/// ```
29/// # #[cfg(feature = "postgres")]
30/// # mod demo {
31/// use ruststream::prelude::*;
32/// use ruststream_sqlx::keys::Attempt;
33/// # use ruststream_sqlx::{Inbox, InboxQueue};
34/// # #[derive(Inbox, sqlx::FromRow)]
35/// # #[inbox(table = "jobs")]
36/// # pub struct Job { #[field(id)] id: i64, #[field(attempt)] attempt: i16, #[field(payload)] payload: Vec<u8> }
37/// # #[derive(serde::Deserialize)]
38/// # struct Report { id: u64 }
39///
40/// #[subscriber(InboxQueue::<Job>::new("reports"))]
41/// async fn render(report: &Report, Ctx(attempt): Ctx<Attempt>) -> HandlerOutcome {
42/// tracing::info!(report.id, ?attempt, "rendering");
43/// HandlerOutcome::ack()
44/// }
45/// # }
46/// # fn main() {}
47/// ```
48#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
49pub struct InboxContext {
50 attempt: Option<u64>,
51}
52
53impl<M: IncomingMessage> BuildContext<M> for InboxContext {
54 fn build(msg: &M) -> Self {
55 Self {
56 attempt: msg.redelivery_count(),
57 }
58 }
59}
60
61/// What every delivery of an `InboxQueue` subscription on `DB` lends: the context
62/// `Ctx<keys::Pool<DB>>` reads, and `Ctx<keys::Attempt>` after it.
63///
64/// It is built only for a handler whose first `Ctx` reads it, and copies the pool's reference
65/// and the attempt.
66///
67/// # Examples
68///
69/// ```
70/// # #[cfg(feature = "postgres")]
71/// # mod demo {
72/// use ruststream_sqlx::prelude::*;
73/// use sqlx::Postgres;
74/// # #[derive(Inbox, sqlx::FromRow)]
75/// # #[inbox(table = "jobs")]
76/// # pub struct Job { #[field(id)] id: i64, #[field(attempt)] attempt: i16, #[field(payload)] payload: Vec<u8> }
77/// # #[derive(serde::Deserialize)]
78/// # struct Upload { name: String }
79///
80/// // `keys::Pool` comes first, so the handler's context is `PoolContext<Postgres>`.
81/// #[subscriber(InboxQueue::<Job>::new("uploads"))]
82/// async fn scan(
83/// upload: &Upload,
84/// Ctx(pool): Ctx<keys::Pool<Postgres>>,
85/// Ctx(attempt): Ctx<keys::Attempt>,
86/// ) -> HandlerOutcome {
87/// let logged = sqlx::query("INSERT INTO scans (name, attempt) VALUES ($1, $2)")
88/// .bind(&upload.name)
89/// .bind(attempt.and_then(|attempt| i64::try_from(attempt).ok()))
90/// .execute(&pool)
91/// .await;
92/// if logged.is_ok() { HandlerOutcome::ack() } else { HandlerOutcome::retry() }
93/// }
94/// # }
95/// # fn main() {}
96/// ```
97pub struct PoolContext<DB: QueueDatabase> {
98 pool: &'static ServicePool<DB>,
99 attempt: Option<u64>,
100}
101
102impl<DB: QueueDatabase> Clone for PoolContext<DB> {
103 fn clone(&self) -> Self {
104 *self
105 }
106}
107
108impl<DB: QueueDatabase> Copy for PoolContext<DB> {}
109
110impl<DB: QueueDatabase> fmt::Debug for PoolContext<DB> {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 f.debug_struct("PoolContext")
113 .field("attempt", &self.attempt)
114 .finish_non_exhaustive()
115 }
116}
117
118impl<DB, Row, Mode> BuildContext<InboxDelivery<DB, Row, Mode>> for PoolContext<DB>
119where
120 DB: QueueDatabase,
121 Row: Events<DB>,
122 Mode: InboxMode,
123{
124 fn build(msg: &InboxDelivery<DB, Row, Mode>) -> Self {
125 Self {
126 pool: msg.pool(),
127 attempt: msg.redelivery_count(),
128 }
129 }
130}
131
132/// What a transactional delivery on `DB` lends its handler: the context `Ctx<keys::Tx<DB>>`
133/// reads, and `Ctx<keys::Pool<DB>>` and `Ctx<keys::Attempt>` after it.
134///
135/// It is built only for a handler whose first `Ctx` reads it, and copies two references, where
136/// the delivery's transaction waits and the pool, with the transaction's slot and the attempt.
137///
138/// # Examples
139///
140/// ```
141/// # #[cfg(feature = "postgres")]
142/// # mod demo {
143/// use ruststream_sqlx::prelude::*;
144/// use sqlx::{PgPool, Postgres};
145/// # #[derive(Inbox, sqlx::FromRow)]
146/// # #[inbox(table = "jobs")]
147/// # pub struct Job { #[field(id)] id: i64, #[field(attempt)] attempt: i16, #[field(payload)] payload: Vec<u8> }
148/// # #[derive(serde::Deserialize)]
149/// # struct Transfer { from: String, to: String, cents: i64 }
150///
151/// // `keys::Tx` comes first, so the handler's context is `TxContext<Postgres>`, and the keys
152/// // after it read the same context.
153/// #[subscriber(InboxQueue::<Job>::new("transfers"))]
154/// async fn transfer(
155/// order: &Transfer,
156/// Ctx(mut tx): Ctx<keys::Tx<Postgres>>,
157/// Ctx(pool): Ctx<keys::Pool<Postgres>>,
158/// Ctx(attempt): Ctx<keys::Attempt>,
159/// ) -> HandlerOutcome {
160/// // Through the pool: the attempt stays logged whatever the outcome.
161/// let _ = sqlx::query("INSERT INTO transfer_log (sender, attempt) VALUES ($1, $2)")
162/// .bind(&order.from)
163/// .bind(attempt.and_then(|attempt| i64::try_from(attempt).ok()))
164/// .execute(&pool)
165/// .await;
166/// // Through the transaction: both legs commit with the acknowledgement, or neither does.
167/// let debited = sqlx::query("UPDATE accounts SET cents = cents - $1 WHERE name = $2")
168/// .bind(order.cents)
169/// .bind(&order.from)
170/// .execute(&mut *tx)
171/// .await;
172/// if debited.is_err() {
173/// return HandlerOutcome::retry();
174/// }
175/// let credited = sqlx::query("UPDATE accounts SET cents = cents + $1 WHERE name = $2")
176/// .bind(order.cents)
177/// .bind(&order.to)
178/// .execute(&mut *tx)
179/// .await;
180/// if credited.is_ok() { HandlerOutcome::ack() } else { HandlerOutcome::retry() }
181/// }
182///
183/// pub fn app(pool: PgPool) -> RustStream {
184/// RustStream::new(AppInfo::new("bank", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
185/// b.include(transfer.transactional());
186/// })
187/// }
188/// # }
189/// # fn main() {}
190/// ```
191pub struct TxContext<DB: QueueDatabase> {
192 lender: Lender<DB>,
193 pool: &'static ServicePool<DB>,
194 attempt: Option<u64>,
195}
196
197impl<DB: QueueDatabase> TxContext<DB> {
198 /// The context of a delivery whose transaction `lender` lends, on `pool`, at `attempt`.
199 pub(crate) const fn new(
200 lender: Lender<DB>,
201 pool: &'static ServicePool<DB>,
202 attempt: Option<u64>,
203 ) -> Self {
204 Self {
205 lender,
206 pool,
207 attempt,
208 }
209 }
210}
211
212impl<DB: QueueDatabase> Clone for TxContext<DB> {
213 fn clone(&self) -> Self {
214 *self
215 }
216}
217
218impl<DB: QueueDatabase> Copy for TxContext<DB> {}
219
220impl<DB: QueueDatabase> fmt::Debug for TxContext<DB> {
221 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222 f.debug_struct("TxContext")
223 .field("lender", &self.lender)
224 .field("attempt", &self.attempt)
225 .finish_non_exhaustive()
226 }
227}
228
229/// A delivery that lends its transaction to its handler: one of a transactional subscription.
230/// Machinery; it gates the [`TxContext`] a handler taking `Ctx<keys::Tx<..>>` reads, so mounting
231/// that handler without transactional mode does not compile.
232#[doc(hidden)]
233#[diagnostic::on_unimplemented(
234 message = "the handler takes `Ctx<keys::Tx<..>>`, and `{Self}` comes from a subscription \
235 without transactional mode",
236 label = "the delivery's transaction is lent only in transactional mode",
237 note = "mount the handler with `.transactional()`: `b.include(handler.transactional())`"
238)]
239pub trait TransactionalDelivery<DB: QueueDatabase> {
240 /// The context the delivery lends its handler.
241 fn tx_context(&self) -> TxContext<DB>;
242}
243
244impl<DB, Row, Mode> BuildContext<InboxDelivery<DB, Row, Mode>> for TxContext<DB>
245where
246 DB: QueueDatabase,
247 Row: Events<DB>,
248 InboxDelivery<DB, Row, Mode>: TransactionalDelivery<DB>,
249{
250 fn build(msg: &InboxDelivery<DB, Row, Mode>) -> Self {
251 msg.tx_context()
252 }
253}
254
255/// The delivery's attempt, 1 for the first delivery: the row's `attempt` as it stood before the
256/// claim; `None` for a table without one.
257///
258/// Retry backoff is the handler's: it reads the attempt and answers with
259/// `HandlerOutcome::retry_after(..)`.
260///
261/// # Examples
262///
263/// ```
264/// # #[cfg(feature = "postgres")]
265/// # mod demo {
266/// use std::time::Duration;
267///
268/// use ruststream::prelude::*;
269/// use ruststream_sqlx::keys::Attempt;
270/// # use ruststream_sqlx::{Inbox, InboxQueue};
271/// # #[derive(Inbox, sqlx::FromRow)]
272/// # #[inbox(table = "jobs")]
273/// # pub struct Job { #[field(id)] id: i64, #[field(retry_after)] retry_after: chrono::DateTime<chrono::Utc>, #[field(attempt)] attempt: i16, #[field(payload)] payload: Vec<u8> }
274/// # #[derive(serde::Deserialize)]
275/// # struct Charge { order: u64 }
276/// # async fn try_charge(_: &Charge) -> bool { true }
277///
278/// #[subscriber(InboxQueue::<Job>::new("charges"))]
279/// async fn charge(request: &Charge, Ctx(attempt): Ctx<Attempt>) -> HandlerOutcome {
280/// if try_charge(request).await {
281/// return HandlerOutcome::ack();
282/// }
283/// // Doubling backoff: 1s, 2s, 4s, ...
284/// let exponent = u32::try_from(attempt.unwrap_or(1).min(16)).unwrap_or(16);
285/// HandlerOutcome::retry_after(Duration::from_secs(1 << (exponent - 1)))
286/// }
287/// # }
288/// # fn main() {}
289/// ```
290#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
291pub struct Attempt;
292
293impl ContextField for Attempt {
294 type Context = InboxContext;
295 type Value = Option<u64>;
296
297 fn read(self, src: &InboxContext) -> Option<u64> {
298 src.attempt
299 }
300}
301
302/// The delivery's transaction: the handler's first `Ctx`, on a registration mounted with
303/// `.transactional()`.
304///
305/// It reads [`TxContext`] and yields [`Tx`](crate::Tx), which goes back to the delivery when the
306/// handler ends. A handler reads it once: a second read for one delivery panics, the transaction
307/// lent already.
308///
309/// # Examples
310///
311/// ```
312/// # #[cfg(feature = "postgres")]
313/// # mod demo {
314/// use ruststream_sqlx::prelude::*;
315/// use sqlx::{PgPool, Postgres};
316/// # #[derive(Inbox, sqlx::FromRow)]
317/// # #[inbox(table = "jobs")]
318/// # pub struct Job { #[field(id)] id: i64, #[field(payload)] payload: Vec<u8> }
319/// # #[derive(serde::Deserialize)]
320/// # struct Shipment { order: i64 }
321///
322/// #[subscriber(InboxQueue::<Job>::new("shipments"))]
323/// async fn ship(shipment: &Shipment, Ctx(mut tx): Ctx<keys::Tx<Postgres>>) -> HandlerOutcome {
324/// let shipped = sqlx::query("UPDATE orders SET shipped = true WHERE id = $1")
325/// .bind(shipment.order)
326/// .execute(&mut *tx)
327/// .await;
328/// if shipped.is_ok() { HandlerOutcome::ack() } else { HandlerOutcome::retry() }
329/// }
330///
331/// pub fn app(pool: PgPool) -> RustStream {
332/// RustStream::new(AppInfo::new("shipping", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
333/// b.include(ship.transactional());
334/// })
335/// }
336/// # }
337/// # fn main() {}
338/// ```
339pub struct Tx<DB>(PhantomData<fn() -> DB>);
340
341impl<DB> Default for Tx<DB> {
342 fn default() -> Self {
343 Self(PhantomData)
344 }
345}
346
347impl<DB> Clone for Tx<DB> {
348 fn clone(&self) -> Self {
349 *self
350 }
351}
352
353impl<DB> Copy for Tx<DB> {}
354
355impl<DB> fmt::Debug for Tx<DB> {
356 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
357 f.write_str("Tx")
358 }
359}
360
361impl<DB: QueueDatabase> ContextField for Tx<DB> {
362 type Context = TxContext<DB>;
363 type Value = super::Tx<DB>;
364
365 /// Lends the delivery's transaction: one lock, no allocation.
366 ///
367 /// # Panics
368 ///
369 /// When the transaction is lent already: a handler reads it once per delivery.
370 fn read(self, src: &TxContext<DB>) -> super::Tx<DB> {
371 super::Tx::lent_by(src.lender).expect(
372 "the delivery's transaction is lent already: a handler reads `Ctx<keys::Tx<..>>` \
373 once per delivery",
374 )
375 }
376}
377
378/// The pool a handler queries through: writes through it commit on their own, whatever the
379/// delivery's outcome.
380///
381/// It reads [`PoolContext`], which every delivery of an `InboxQueue` subscription lends, or
382/// [`TxContext`] after `Ctx<keys::Tx<..>>`. On the app's runtime it yields the service's pool.
383/// On a dedicated thread it yields the thread's own pool, built from the service pool's options
384/// on the first read there. Its connections open on the thread and close with it, so none
385/// outlives the thread's runtime in the service's pool. It opens one connection at most, or the
386/// number [`InboxThreads::connections`](crate::InboxThreads::connections) sets. A read costs one
387/// thread-local access and one clone of a pool handle. A pool's type names its database, and so does the key: `keys::Pool<Postgres>`.
388///
389/// # Examples
390///
391/// ```
392/// # #[cfg(feature = "postgres")]
393/// # mod demo {
394/// use ruststream_sqlx::prelude::*;
395/// use sqlx::Postgres;
396/// # #[derive(Inbox, sqlx::FromRow)]
397/// # #[inbox(table = "jobs")]
398/// # pub struct Job { #[field(id)] id: i64, #[field(payload)] payload: Vec<u8> }
399/// # #[derive(serde::Deserialize)]
400/// # struct Visit { page: String }
401///
402/// #[subscriber(InboxQueue::<Job>::new("visits"))]
403/// async fn count(visit: &Visit, Ctx(pool): Ctx<keys::Pool<Postgres>>) -> HandlerOutcome {
404/// let counted = sqlx::query("UPDATE pages SET visits = visits + 1 WHERE path = $1")
405/// .bind(&visit.page)
406/// .execute(&pool)
407/// .await;
408/// if counted.is_ok() { HandlerOutcome::ack() } else { HandlerOutcome::retry() }
409/// }
410/// # }
411/// # fn main() {}
412/// ```
413pub struct Pool<DB>(PhantomData<fn() -> DB>);
414
415impl<DB> Default for Pool<DB> {
416 fn default() -> Self {
417 Self(PhantomData)
418 }
419}
420
421impl<DB> Clone for Pool<DB> {
422 fn clone(&self) -> Self {
423 *self
424 }
425}
426
427impl<DB> Copy for Pool<DB> {}
428
429impl<DB> fmt::Debug for Pool<DB> {
430 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431 f.write_str("Pool")
432 }
433}
434
435impl<DB: QueueDatabase> ContextField for Pool<DB> {
436 type Context = PoolContext<DB>;
437 type Value = DbPool<DB>;
438
439 fn read(self, src: &PoolContext<DB>) -> DbPool<DB> {
440 src.pool.for_handler()
441 }
442}
443
444/// Reads the attempt off a context richer than the one [`Attempt`] names.
445#[derive(Clone, Copy)]
446struct AttemptOf;
447
448impl<DB: QueueDatabase> Field<PoolContext<DB>> for AttemptOf {
449 type Value<'a>
450 = Option<u64>
451 where
452 DB: 'a;
453
454 fn get(self, src: &PoolContext<DB>) -> Option<u64> {
455 src.attempt
456 }
457}
458
459impl<DB: QueueDatabase> Field<TxContext<DB>> for AttemptOf {
460 type Value<'a>
461 = Option<u64>
462 where
463 DB: 'a;
464
465 fn get(self, src: &TxContext<DB>) -> Option<u64> {
466 src.attempt
467 }
468}
469
470/// Reads the pool off a transactional delivery's context.
471#[derive(Clone, Copy)]
472struct PoolOf;
473
474impl<DB: QueueDatabase> Field<TxContext<DB>> for PoolOf {
475 type Value<'a>
476 = DbPool<DB>
477 where
478 DB: 'a;
479
480 fn get(self, src: &TxContext<DB>) -> DbPool<DB> {
481 src.pool.for_handler()
482 }
483}
484
485// The reads of a later key from an earlier key's context. The framework's own impl reads a key
486// from the one context the key names; these let the inbox's richer contexts serve the keys they
487// carry as well, and none overlaps it: each names a context other than the key's own.
488
489impl<S: Sync, DB: QueueDatabase> FromContext<PoolContext<DB>, S> for Ctx<Attempt> {
490 type Rejection = Infallible;
491
492 fn from_context(
493 ctx: &mut Context<'_, PoolContext<DB>, S>,
494 ) -> impl Future<Output = Result<Self, Infallible>> + Send {
495 let attempt = ctx.context(AttemptOf);
496 async move { Ok(Self(attempt)) }
497 }
498}
499
500impl<S: Sync, DB: QueueDatabase> FromContext<TxContext<DB>, S> for Ctx<Attempt> {
501 type Rejection = Infallible;
502
503 fn from_context(
504 ctx: &mut Context<'_, TxContext<DB>, S>,
505 ) -> impl Future<Output = Result<Self, Infallible>> + Send {
506 let attempt = ctx.context(AttemptOf);
507 async move { Ok(Self(attempt)) }
508 }
509}
510
511impl<S: Sync, DB: QueueDatabase> FromContext<TxContext<DB>, S> for Ctx<Pool<DB>> {
512 type Rejection = Infallible;
513
514 fn from_context(
515 ctx: &mut Context<'_, TxContext<DB>, S>,
516 ) -> impl Future<Output = Result<Self, Infallible>> + Send {
517 let pool = ctx.context(PoolOf);
518 async move { Ok(Self(pool)) }
519 }
520}
521
522#[cfg(all(test, feature = "sqlite"))]
523mod tests {
524 use ruststream::ContextField;
525 use sqlx::sqlite::SqlitePoolOptions;
526 use sqlx::{Error, SqlitePool};
527
528 use std::num::NonZeroU32;
529
530 use tokio::runtime::Handle;
531
532 use super::{Tx, TxContext};
533 use crate::inbox::pools::ServicePool;
534 use crate::inbox::transactional::TxBook;
535 use crate::inbox::tx::PoolTx;
536
537 #[tokio::test]
538 #[should_panic(expected = "lent already")]
539 async fn a_second_read_of_the_transaction_panics() {
540 let pool: SqlitePool = SqlitePoolOptions::new()
541 .connect("sqlite::memory:")
542 .await
543 .expect("an in-memory database opens");
544 let pool = ServicePool::leak(pool, Handle::current(), NonZeroU32::MIN);
545 let opened: Result<PoolTx<_>, Error> = PoolTx::begin(pool, None).await;
546 let hold = TxBook::leak().enter(opened.expect("begins"));
547 let cx = TxContext::new(hold.lender(), pool, Some(1));
548 let _first = Tx::default().read(&cx);
549 let _second = Tx::default().read(&cx);
550 }
551}