reliar_store_postgres/inbox/
dead_letters.rs1use reliar_inbox::{InboxDeadLetters, InboxDeadQuery, InboxRecord, InboxRecordId};
5use tracing::Instrument as _;
6
7use super::error::PostgresInboxError;
8
9use super::PostgresInboxStore;
10use super::purge::{InboxRow, build_record};
11
12const MAX_LIST_DEAD_LIMIT: u32 = 1000;
17
18impl InboxDeadLetters for PostgresInboxStore {
19 type Error = PostgresInboxError;
20
21 fn list_dead(
26 &self,
27 query: InboxDeadQuery,
28 ) -> impl Future<Output = Result<Vec<InboxRecord>, Self::Error>> + Send {
29 let capped_limit = query.limit.min(MAX_LIST_DEAD_LIMIT);
33 let limit = i64::from(capped_limit);
34 let scope_owned = query.scope.clone();
35 let dead_before = query.dead_before;
36 let (after_dead_at, after_id) = query.after.map_or((None, None), |cursor| {
37 (Some(cursor.dead_at()), Some(cursor.id().as_uuid()))
38 });
39 let message_type = query.message_type.clone();
40
41 let span = tracing::debug_span!(
42 "reliar.inbox.list_dead",
43 inbox.scope = scope_owned.as_ref().map(reliar_inbox::InboxScope::as_str),
44 inbox.limit = capped_limit,
45 inbox.returned = tracing::field::Empty,
46 );
47 let recording_span = span.clone();
48
49 async move {
50 let scope = scope_owned.as_ref().map(reliar_inbox::InboxScope::as_str);
51
52 let rows = if self.settings.statement_timeout.is_zero() {
53 list_dead_rows(
54 &self.pool,
55 scope,
56 message_type.as_deref(),
57 dead_before,
58 after_dead_at,
59 after_id,
60 limit,
61 )
62 .await?
63 } else {
64 let mut tx = self.pool.begin().await?;
65
66 self.set_local_timeout(&mut tx).await?;
67 let rows = list_dead_rows(
68 &mut *tx,
69 scope,
70 message_type.as_deref(),
71 dead_before,
72 after_dead_at,
73 after_id,
74 limit,
75 )
76 .await?;
77 tx.commit().await?;
78
79 rows
80 };
81
82 recording_span.record("inbox.returned", rows.len());
83
84 rows.into_iter().map(build_record).collect()
85 }
86 .instrument(span)
87 }
88
89 fn retry_dead(
92 &self,
93 ids: &[InboxRecordId],
94 ) -> impl Future<Output = Result<u64, Self::Error>> + Send {
95 let requested = ids.len();
96 let ids: Vec<uuid::Uuid> = ids
97 .iter()
98 .map(reliar_inbox::InboxRecordId::as_uuid)
99 .collect();
100
101 let span = tracing::debug_span!(
102 "reliar.inbox.retry_dead",
103 inbox.requested = requested,
104 inbox.affected = tracing::field::Empty,
105 );
106 let recording_span = span.clone();
107
108 async move {
109 if ids.is_empty() {
110 recording_span.record("inbox.affected", 0_u64);
111
112 return Ok(0);
113 }
114
115 let result = if self.settings.statement_timeout.is_zero() {
116 retry_dead_rows(&self.pool, &ids).await?
117 } else {
118 let mut tx = self.pool.begin().await?;
119
120 self.set_local_timeout(&mut tx).await?;
121 let result = retry_dead_rows(&mut *tx, &ids).await?;
122 tx.commit().await?;
123
124 result
125 };
126
127 recording_span.record("inbox.affected", result);
128
129 Ok(result)
130 }
131 .instrument(span)
132 }
133
134 fn purge_dead(
137 &self,
138 ids: &[InboxRecordId],
139 ) -> impl Future<Output = Result<u64, Self::Error>> + Send {
140 let requested = ids.len();
141 let ids: Vec<uuid::Uuid> = ids
142 .iter()
143 .map(reliar_inbox::InboxRecordId::as_uuid)
144 .collect();
145
146 let span = tracing::debug_span!(
147 "reliar.inbox.purge_dead",
148 inbox.requested = requested,
149 inbox.affected = tracing::field::Empty,
150 );
151 let recording_span = span.clone();
152
153 async move {
154 if ids.is_empty() {
155 recording_span.record("inbox.affected", 0_u64);
156
157 return Ok(0);
158 }
159
160 let result = if self.settings.statement_timeout.is_zero() {
161 purge_dead_rows(&self.pool, &ids).await?
162 } else {
163 let mut tx = self.pool.begin().await?;
164
165 self.set_local_timeout(&mut tx).await?;
166 let result = purge_dead_rows(&mut *tx, &ids).await?;
167 tx.commit().await?;
168
169 result
170 };
171
172 recording_span.record("inbox.affected", result);
173
174 Ok(result)
175 }
176 .instrument(span)
177 }
178}
179
180#[allow(
181 clippy::too_many_arguments,
182 reason = "each argument is one InboxDeadQuery filter; a struct wrapper would just move the \
183 same six names one level down"
184)]
185async fn list_dead_rows<'e>(
186 executor: impl sqlx::PgExecutor<'e>,
187 scope: Option<&str>,
188 message_type: Option<&str>,
189 dead_before: Option<time::OffsetDateTime>,
190 after_dead_at: Option<time::OffsetDateTime>,
191 after_id: Option<uuid::Uuid>,
192 limit: i64,
193) -> Result<Vec<InboxRow>, sqlx::Error> {
194 sqlx::query_as!(
195 InboxRow,
196 r#"SELECT id, scope, message_id, message_type, message_version, conversation_id,
197 correlation_id, causation_id, received_at, updated_at, completed_at, dead_at,
198 attempts, last_error
199 FROM inbox
200 WHERE dead_at IS NOT NULL
201 AND ($1::text IS NULL OR scope = $1)
202 AND ($2::text IS NULL OR message_type = $2)
203 AND ($3::timestamptz IS NULL OR dead_at < $3)
204 AND ($4::timestamptz IS NULL OR (dead_at, id) > ($4, $5::uuid))
205 ORDER BY dead_at, id
206 LIMIT $6"#,
207 scope,
208 message_type,
209 dead_before,
210 after_dead_at,
211 after_id,
212 limit,
213 )
214 .fetch_all(executor)
215 .await
216}
217
218async fn retry_dead_rows<'e>(
219 executor: impl sqlx::PgExecutor<'e>,
220 ids: &[uuid::Uuid],
221) -> Result<u64, sqlx::Error> {
222 let result = sqlx::query!(
223 r#"UPDATE inbox SET dead_at = NULL, attempts = 0, updated_at = now()
224 WHERE id = ANY($1) AND dead_at IS NOT NULL"#,
225 ids,
226 )
227 .execute(executor)
228 .await?;
229
230 Ok(result.rows_affected())
231}
232
233async fn purge_dead_rows<'e>(
234 executor: impl sqlx::PgExecutor<'e>,
235 ids: &[uuid::Uuid],
236) -> Result<u64, sqlx::Error> {
237 let result = sqlx::query!(
238 r#"DELETE FROM inbox WHERE id = ANY($1) AND dead_at IS NOT NULL"#,
239 ids,
240 )
241 .execute(executor)
242 .await?;
243
244 Ok(result.rows_affected())
245}