1use 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
19pub 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
74struct 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 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#[derive(Clone, Copy)]
109enum Settlement {
110 Ack,
111 Retry,
112 Discard,
113}
114
115impl Settlement {
116 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
145pub(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 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}