Skip to main content

ruststream_sqlx/outbox/
layer.rs

1//! The subscription middleware: a delivery under a registered name that carries an id takes its
2//! record before the handler runs, and settles it by the handler's outcome.
3
4use std::any::type_name;
5use std::fmt;
6use std::sync::Arc;
7
8use ruststream::runtime::{BlanketLayer, Context, Handler, HandlerOutcome};
9use sqlx::{Database, Error, Pool};
10use tracing::warn;
11
12use super::database::Defaults;
13use super::events::Tracked;
14use super::publish::carried_id;
15use super::registry::RecordList;
16use super::switch::enabled;
17use crate::outbox::store::Store;
18
19/// The subscription middleware of an [`Outbox`](super::Outbox), from
20/// [`layer`](super::Outbox::layer).
21///
22/// A delivery under a registered name that carries [`OUTBOX_ID_HEADER`](super::OUTBOX_ID_HEADER)
23/// takes its record with `Fetch` before the handler runs: a record taken or processed already is
24/// acknowledged without the handler, and a fetch that fails, or a missing pool, retries the
25/// delivery without it. After the handler, `Ack`, `Retry` or `Discard` settles the record by the
26/// outcome; a settlement that fails is logged, and the handler's outcome stands.
27///
28/// A delivery under any other name, or without the header, runs the handler as it is, and its
29/// headers are not read.
30pub struct TrackingLayer<DB: Database, Records> {
31    store: Arc<Store<DB>>,
32    records: Records,
33}
34
35impl<DB: Database, Records> TrackingLayer<DB, Records> {
36    pub(super) const fn new(store: Arc<Store<DB>>, records: Records) -> Self {
37        Self { store, records }
38    }
39}
40
41impl<DB: Database, Records: Copy> Clone for TrackingLayer<DB, Records> {
42    fn clone(&self) -> Self {
43        Self {
44            store: Arc::clone(&self.store),
45            records: self.records,
46        }
47    }
48}
49
50impl<DB: Database, Records: fmt::Debug> fmt::Debug for TrackingLayer<DB, Records> {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        f.debug_struct("TrackingLayer")
53            .field("records", &self.records)
54            .finish_non_exhaustive()
55    }
56}
57
58impl<DB: Database, Records: RecordList<DB>> BlanketLayer for TrackingLayer<DB, Records> {
59    fn apply<M, C, S, H>(&self, handler: H) -> impl Handler<M, C, S> + 'static
60    where
61        M: Send + Sync + 'static,
62        C: Send + 'static,
63        S: Send + Sync + 'static,
64        H: Handler<M, C, S> + 'static,
65    {
66        Tracking {
67            inner: handler,
68            store: Arc::clone(&self.store),
69            records: self.records,
70        }
71    }
72}
73
74/// A handler wrapped by [`TrackingLayer`].
75struct Tracking<H, DB: Database, Records> {
76    inner: H,
77    store: Arc<Store<DB>>,
78    records: Records,
79}
80
81impl<M, C, S, H, DB, Records> Handler<M, C, S> for Tracking<H, DB, Records>
82where
83    M: Sync,
84    C: Send,
85    S: Send + Sync,
86    H: Handler<M, C, S>,
87    DB: Database,
88    Records: RecordList<DB>,
89{
90    async fn handle(&self, msg: &M, ctx: &mut Context<'_, C, S>) -> HandlerOutcome {
91        if !enabled() {
92            return self.inner.handle(msg, ctx).await;
93        }
94        // The service's runtime, for the publishes of handlers on dedicated threads: the
95        // context of every delivery names it.
96        self.store.learn_home(ctx.main_runtime().as_handle());
97        if self.records.contains(ctx.name()) {
98            self.records
99                .deliver(&self.store, &self.inner, msg, ctx)
100                .await
101        } else {
102            self.inner.handle(msg, ctx).await
103        }
104    }
105}
106
107/// The event that settles a record after its handler.
108#[derive(Clone, Copy)]
109enum Settlement {
110    Ack,
111    Retry,
112    Discard,
113}
114
115impl Settlement {
116    /// The event of `outcome` for `Record`, or `None` when it writes nothing.
117    fn of<DB: Database, Record: Tracked<DB>>(outcome: &HandlerOutcome) -> Option<Self> {
118        if outcome.is_ack() {
119            Some(Self::Ack)
120        } else if outcome.is_drop() {
121            Some(Self::Discard)
122        } else if outcome.is_retry() && Record::RETRY_WRITES {
123            Some(Self::Retry)
124        } else {
125            None
126        }
127    }
128
129    async fn run<DB: Database, Record: Tracked<DB>>(
130        self,
131        store: &Store<DB>,
132        pool: &Pool<DB>,
133        id: &Record::Id,
134        defaults: &Defaults,
135    ) -> Result<(), Error> {
136        let mut conn = store.acquire(pool).await?;
137        match self {
138            Self::Ack => Record::ack_record(&mut conn, id, defaults).await,
139            Self::Retry => Record::retry_record(&mut conn, id, defaults).await,
140            Self::Discard => Record::discard_record(&mut conn, id, defaults).await,
141        }
142    }
143}
144
145/// Takes the record `Record` of a delivery under `name`, runs `handler`, and settles the record.
146pub(super) async fn deliver<DB, Record, M, C, S, H>(
147    name: &'static str,
148    defaults: &Defaults,
149    store: &Store<DB>,
150    handler: &H,
151    msg: &M,
152    ctx: &mut Context<'_, C, S>,
153) -> HandlerOutcome
154where
155    DB: Database,
156    Record: Tracked<DB>,
157    M: Sync,
158    C: Send,
159    S: Send + Sync,
160    H: Handler<M, C, S>,
161{
162    let record = type_name::<Record>();
163    let id = match carried_id::<Record::Id>(ctx.headers()) {
164        None => return handler.handle(msg, ctx).await,
165        Some(Ok(id)) => id,
166        Some(Err(text)) => {
167            warn!(
168                target: "ruststream_sqlx",
169                subscription = name,
170                record,
171                id = text,
172                "the outbox id does not parse as the record's id; the delivery runs untracked",
173            );
174            return handler.handle(msg, ctx).await;
175        }
176    };
177    let Some(pool) = store.get() else {
178        warn!(
179            target: "ruststream_sqlx",
180            subscription = name,
181            record,
182            id = %id,
183            "the outbox has no pool yet; the delivery is retried without its handler",
184        );
185        return HandlerOutcome::retry();
186    };
187    // The connection goes back before the handler runs: the handler may publish tracked messages,
188    // and each of those takes one.
189    let taken = async {
190        let mut conn = store.acquire(pool).await?;
191        Record::fetch_record(&mut conn, &id, defaults).await
192    }
193    .await;
194    match taken {
195        Ok(Some(_)) => {}
196        Ok(None) => return HandlerOutcome::ack(),
197        Err(error) => {
198            warn!(
199                target: "ruststream_sqlx",
200                subscription = name,
201                record,
202                id = %id,
203                %error,
204                "the outbox did not take the record; the delivery is retried without its handler",
205            );
206            return HandlerOutcome::retry();
207        }
208    }
209    let outcome = handler.handle(msg, ctx).await;
210    if let Some(settlement) = Settlement::of::<DB, Record>(&outcome)
211        && let Err(error) = settlement
212            .run::<DB, Record>(store, pool, &id, defaults)
213            .await
214    {
215        warn!(
216            target: "ruststream_sqlx",
217            subscription = name,
218            record,
219            id = %id,
220            %error,
221            "the outbox did not settle the record; it stays for the next startup",
222        );
223    }
224    outcome
225}