1use crate::{
9 error::PageError,
10 hydrate::{EventRow, hydrate_event},
11 rls,
12};
13use dovecote::{
14 AttemptCount, DeliverySnapshot, Failure, Limit, PagedEvent, QuarantineReason, RowId, TenantId,
15 WorkerId,
16};
17use sqlx::{FromRow, PgConnection, PgPool, Postgres, Transaction, query_as, query_scalar};
18use std::marker::PhantomData;
19use time::OffsetDateTime;
20
21pub(crate) async fn page_for_scope(
22 pool: &PgPool,
23 tenant_id: Option<&TenantId>,
24 after_row_id: Option<RowId>,
25 limit: Limit,
26) -> Result<Vec<PagedEvent>, PageError> {
27 if let Some(tenant_id) = tenant_id {
28 let mut transaction = pool
29 .begin()
30 .await
31 .map_err(|source| PageError::sql("begin scoped live page transaction", source))?;
32 rls::bind_tenant(&mut transaction, tenant_id)
33 .await
34 .map_err(|source| PageError::sql("bind live page tenant", source))?;
35 let result = query_page_on_connection(
36 &mut transaction,
37 Some(tenant_id),
38 after_row_id.map_or(0, RowId::get),
39 None,
40 limit,
41 )
42 .await;
43 return match result {
44 Ok(rows) => {
45 transaction.commit().await.map_err(|source| {
46 PageError::sql("finish scoped live page transaction", source)
47 })?;
48 Ok(rows)
49 }
50 Err(error) => {
51 let _ = transaction.rollback().await;
52 Err(error)
53 }
54 };
55 }
56
57 let mut connection = pool
58 .acquire()
59 .await
60 .map_err(|source| PageError::sql("acquire live page connection", source))?;
61 query_page_on_connection(
62 &mut connection,
63 tenant_id,
64 after_row_id.map_or(0, RowId::get),
65 None,
66 limit,
67 )
68 .await
69}
70
71pub(crate) async fn begin_snapshot_for_scope(
72 pool: &PgPool,
73 tenant_id: Option<&TenantId>,
74) -> Result<SnapshotPager, PageError> {
75 let mut transaction = pool
76 .begin_with(sqlx::AssertSqlSafe(
77 "BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY",
78 ))
79 .await
80 .map_err(|source| PageError::sql("begin snapshot transaction", source))?;
81 if let Some(tenant_id) = tenant_id {
82 rls::bind_tenant(&mut transaction, tenant_id)
83 .await
84 .map_err(|source| PageError::sql("bind snapshot tenant", source))?;
85 }
86
87 let upper_bound = query_scalar::<_, Option<i64>>(
88 "SELECT MAX(row_id) FROM dovecote_events WHERE ($1::varchar IS NULL OR tenant_id = $1)",
89 )
90 .bind(tenant_id.map(TenantId::as_str))
91 .fetch_one(&mut *transaction)
92 .await
93 .map_err(|source| PageError::sql("read snapshot upper row ID", source))?
94 .map(|value| RowId::new(value).map_err(|error| PageError::serialization(error.to_string())))
95 .transpose()?;
96
97 Ok(SnapshotPager {
98 transaction,
99 upper_bound,
100 cursor: None,
101 exhausted: upper_bound.is_none(),
102 tenant_id: tenant_id.cloned(),
103 _not_send: PhantomData,
104 })
105}
106
107pub struct SnapshotPager {
127 transaction: Transaction<'static, Postgres>,
128 upper_bound: Option<RowId>,
129 cursor: Option<RowId>,
130 exhausted: bool,
131 tenant_id: Option<TenantId>,
132 _not_send: PhantomData<*mut ()>,
133}
134
135impl SnapshotPager {
136 pub const fn cursor(&self) -> Option<RowId> {
138 self.cursor
139 }
140
141 pub const fn upper_bound(&self) -> Option<RowId> {
143 self.upper_bound
144 }
145
146 pub const fn is_exhausted(&self) -> bool {
148 self.exhausted
149 }
150
151 pub async fn next_page(&mut self, limit: Limit) -> Result<Vec<PagedEvent>, PageError> {
158 if self.exhausted {
159 return Ok(Vec::new());
160 }
161
162 let upper_bound = self
163 .upper_bound
164 .expect("a non-exhausted pager has an upper bound");
165 let rows = query_page_on_connection(
166 &mut self.transaction,
167 self.tenant_id.as_ref(),
168 self.cursor.map_or(0, RowId::get),
169 Some(upper_bound.get()),
170 limit,
171 )
172 .await?;
173
174 if let Some(last) = rows.last() {
175 self.cursor = Some(last.row_id());
176 if rows.len() < limit.get() as usize || self.cursor == self.upper_bound {
177 self.exhausted = true;
178 }
179 } else {
180 self.exhausted = true;
181 }
182 Ok(rows)
183 }
184
185 pub async fn finish(self) -> Result<(), PageError> {
187 self.transaction
188 .commit()
189 .await
190 .map_err(|source| PageError::sql("finish snapshot transaction", source))
191 }
192
193 pub async fn rollback(self) -> Result<(), PageError> {
195 self.transaction
196 .rollback()
197 .await
198 .map_err(|source| PageError::sql("rollback snapshot transaction", source))
199 }
200
201 pub async fn close(self) -> Result<(), PageError> {
203 self.rollback().await
204 }
205}
206
207async fn query_page_on_connection(
209 connection: &mut PgConnection,
210 tenant_id: Option<&TenantId>,
211 after_row_id: i64,
212 upper_bound: Option<i64>,
213 limit: Limit,
214) -> Result<Vec<PagedEvent>, PageError> {
215 let rows = match upper_bound {
216 Some(upper_bound) => {
217 query_as::<_, PageRow>(SNAPSHOT_PAGE_SQL)
218 .bind(tenant_id.map(TenantId::as_str))
219 .bind(after_row_id)
220 .bind(i64::from(limit.get()))
221 .bind(upper_bound)
222 .fetch_all(&mut *connection)
223 .await
224 }
225 None => {
226 query_as::<_, PageRow>(PAGE_SQL)
227 .bind(tenant_id.map(TenantId::as_str))
228 .bind(after_row_id)
229 .bind(i64::from(limit.get()))
230 .fetch_all(&mut *connection)
231 .await
232 }
233 }
234 .map_err(|source| PageError::sql("read event page", source))?;
235
236 rows.into_iter()
237 .map(hydrate_page)
238 .collect::<Result<Vec<_>, _>>()
239 .map_err(PageError::serialization)
240}
241
242const PAGE_SQL: &str = r#"
246 SELECT e.row_id,
247 e.tenant_id,
248 e.stream,
249 e.specversion,
250 e.event_id,
251 e.source,
252 e.event_type,
253 e.subject,
254 e.occurred_at,
255 e.enqueued_at,
256 e.datacontenttype,
257 e.dataschema,
258 e.partitionkey,
259 e.extensions,
260 e.data_kind,
261 e.data,
262 d.state,
263 d.available_at,
264 d.attempts,
265 d.claim_token,
266 d.claimed_by,
267 d.claim_expires_at,
268 d.last_failure_code,
269 d.last_failure_detail,
270 d.delivered_at,
271 d.quarantined_at,
272 d.quarantine_reason
273 FROM dovecote_events AS e
274 LEFT JOIN dovecote_deliveries AS d
275 ON d.tenant_id = e.tenant_id AND d.event_row_id = e.row_id
276 WHERE ($1::varchar IS NULL OR e.tenant_id = $1) AND e.row_id > $2
277 ORDER BY e.row_id ASC
278 LIMIT $3
279"#;
280
281const SNAPSHOT_PAGE_SQL: &str = r#"
282 SELECT e.row_id,
283 e.tenant_id,
284 e.stream,
285 e.specversion,
286 e.event_id,
287 e.source,
288 e.event_type,
289 e.subject,
290 e.occurred_at,
291 e.enqueued_at,
292 e.datacontenttype,
293 e.dataschema,
294 e.partitionkey,
295 e.extensions,
296 e.data_kind,
297 e.data,
298 d.state,
299 d.available_at,
300 d.attempts,
301 d.claim_token,
302 d.claimed_by,
303 d.claim_expires_at,
304 d.last_failure_code,
305 d.last_failure_detail,
306 d.delivered_at,
307 d.quarantined_at,
308 d.quarantine_reason
309 FROM dovecote_events AS e
310 LEFT JOIN dovecote_deliveries AS d
311 ON d.tenant_id = e.tenant_id AND d.event_row_id = e.row_id
312 WHERE ($1::varchar IS NULL OR e.tenant_id = $1) AND e.row_id > $2 AND e.row_id <= $4
313 ORDER BY e.row_id ASC
314 LIMIT $3
315"#;
316
317#[derive(Debug, FromRow)]
318struct PageRow {
319 row_id: i64,
320 tenant_id: String,
321 stream: String,
322 specversion: String,
323 event_id: String,
324 source: String,
325 event_type: String,
326 subject: Option<String>,
327 occurred_at: Option<OffsetDateTime>,
328 enqueued_at: OffsetDateTime,
329 datacontenttype: Option<String>,
330 dataschema: Option<String>,
331 partitionkey: Option<String>,
332 extensions: String,
333 data_kind: Option<String>,
334 data: Option<Vec<u8>>,
335 state: Option<String>,
336 available_at: Option<OffsetDateTime>,
337 attempts: Option<i64>,
338 claim_token: Option<Vec<u8>>,
339 claimed_by: Option<String>,
340 claim_expires_at: Option<OffsetDateTime>,
341 last_failure_code: Option<String>,
342 last_failure_detail: Option<String>,
343 delivered_at: Option<OffsetDateTime>,
344 quarantined_at: Option<OffsetDateTime>,
345 quarantine_reason: Option<String>,
346}
347
348impl PageRow {
349 fn event_row(&self) -> EventRow {
350 EventRow {
351 stream: self.stream.clone(),
352 specversion: self.specversion.clone(),
353 event_id: self.event_id.clone(),
354 source: self.source.clone(),
355 event_type: self.event_type.clone(),
356 subject: self.subject.clone(),
357 occurred_at: self.occurred_at,
358 datacontenttype: self.datacontenttype.clone(),
359 dataschema: self.dataschema.clone(),
360 partitionkey: self.partitionkey.clone(),
361 extensions: self.extensions.clone(),
362 data_kind: self.data_kind.clone(),
363 data: self.data.clone(),
364 }
365 }
366}
367
368fn hydrate_page(row: PageRow) -> Result<PagedEvent, String> {
369 let row_id = RowId::new(row.row_id).map_err(|error| error.to_string())?;
370 let tenant_id = TenantId::new(row.tenant_id.clone()).map_err(|error| error.to_string())?;
371 let event = hydrate_event(&row.event_row())?;
372 let state = row
373 .state
374 .ok_or_else(|| format!("event row {} has no required delivery row", row.row_id))?;
375 let available_at = row
376 .available_at
377 .ok_or_else(|| "delivery row has no available_at".to_owned())?;
378 let attempts = AttemptCount::new(
379 row.attempts
380 .ok_or_else(|| "delivery row has no attempts".to_owned())?,
381 )
382 .map_err(|error| error.to_string())?;
383 let failure = parse_failure(row.last_failure_code, row.last_failure_detail)?;
384 let delivery = match state.as_str() {
385 "pending" => {
386 require_absent("pending claim token", row.claim_token.as_ref())?;
387 require_absent("pending claimed worker", row.claimed_by.as_ref())?;
388 require_absent("pending claim expiry", row.claim_expires_at.as_ref())?;
389 require_absent("pending delivered time", row.delivered_at.as_ref())?;
390 require_absent("pending quarantine time", row.quarantined_at.as_ref())?;
391 require_absent("pending quarantine reason", row.quarantine_reason.as_ref())?;
392 DeliverySnapshot::pending(available_at, attempts, failure)
393 }
394 "claimed" => {
395 require_token_width(row.claim_token.as_deref())?;
396 let worker = row
397 .claimed_by
398 .ok_or_else(|| "claimed delivery has no worker".to_owned())?;
399 let expires_at = row
400 .claim_expires_at
401 .ok_or_else(|| "claimed delivery has no claim expiry".to_owned())?;
402 require_absent("claimed delivered time", row.delivered_at.as_ref())?;
403 require_absent("claimed quarantine time", row.quarantined_at.as_ref())?;
404 require_absent("claimed quarantine reason", row.quarantine_reason.as_ref())?;
405 DeliverySnapshot::claimed(
406 available_at,
407 WorkerId::new(worker).map_err(|error| error.to_string())?,
408 expires_at,
409 attempts,
410 failure,
411 )
412 }
413 "delivered" => {
414 require_absent("delivered claim token", row.claim_token.as_ref())?;
415 require_absent("delivered claimed worker", row.claimed_by.as_ref())?;
416 require_absent("delivered claim expiry", row.claim_expires_at.as_ref())?;
417 let delivered_at = row
418 .delivered_at
419 .ok_or_else(|| "delivered delivery has no delivered time".to_owned())?;
420 require_absent("delivered quarantine time", row.quarantined_at.as_ref())?;
421 require_absent(
422 "delivered quarantine reason",
423 row.quarantine_reason.as_ref(),
424 )?;
425 DeliverySnapshot::delivered(available_at, delivered_at, attempts, failure)
426 }
427 "quarantined" => {
428 require_absent("quarantined claim token", row.claim_token.as_ref())?;
429 require_absent("quarantined claimed worker", row.claimed_by.as_ref())?;
430 require_absent("quarantined claim expiry", row.claim_expires_at.as_ref())?;
431 require_absent("quarantined delivered time", row.delivered_at.as_ref())?;
432 let quarantined_at = row
433 .quarantined_at
434 .ok_or_else(|| "quarantined delivery has no quarantine time".to_owned())?;
435 let reason = row
436 .quarantine_reason
437 .ok_or_else(|| "quarantined delivery has no quarantine reason".to_owned())?;
438 DeliverySnapshot::quarantined(
439 available_at,
440 quarantined_at,
441 attempts,
442 failure,
443 QuarantineReason::new(reason).map_err(|error| error.to_string())?,
444 )
445 }
446 state => return Err(format!("unknown delivery state {state:?}")),
447 }
448 .map_err(|error| error.to_string())?;
449
450 PagedEvent::new(tenant_id, row_id, event, row.enqueued_at, delivery)
451 .map_err(|error| error.to_string())
452}
453
454fn require_absent<T>(field: &str, value: Option<&T>) -> Result<(), String> {
455 if value.is_some() {
456 Err(format!("{field} must be NULL for its delivery state"))
457 } else {
458 Ok(())
459 }
460}
461
462fn require_token_width(value: Option<&[u8]>) -> Result<(), String> {
463 match value {
464 Some(value) if value.len() == dovecote::CLAIM_TOKEN_BYTES => Ok(()),
465 Some(value) => Err(format!(
466 "claimed delivery has an invalid claim token width: {}",
467 value.len()
468 )),
469 None => Err("claimed delivery has no claim token".to_owned()),
470 }
471}
472
473fn parse_failure(code: Option<String>, detail: Option<String>) -> Result<Option<Failure>, String> {
474 match (code, detail) {
475 (None, None) => Ok(None),
476 (Some(code), Some(detail)) => Failure::new(code, detail)
477 .map(Some)
478 .map_err(|error| error.to_string()),
479 _ => Err("delivery failure code and detail must be both NULL or non-NULL".to_owned()),
480 }
481}