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 #[must_use]
138 pub const fn cursor(&self) -> Option<RowId> {
139 self.cursor
140 }
141
142 #[must_use]
144 pub const fn upper_bound(&self) -> Option<RowId> {
145 self.upper_bound
146 }
147
148 #[must_use]
150 pub const fn is_exhausted(&self) -> bool {
151 self.exhausted
152 }
153
154 pub async fn next_page(&mut self, limit: Limit) -> Result<Vec<PagedEvent>, PageError> {
165 if self.exhausted {
166 return Ok(Vec::new());
167 }
168
169 let upper_bound = self
170 .upper_bound
171 .expect("a non-exhausted pager has an upper bound");
172 let rows = query_page_on_connection(
173 &mut self.transaction,
174 self.tenant_id.as_ref(),
175 self.cursor.map_or(0, RowId::get),
176 Some(upper_bound.get()),
177 limit,
178 )
179 .await?;
180
181 if let Some(last) = rows.last() {
182 self.cursor = Some(last.row_id());
183 if rows.len() < limit.get() as usize || self.cursor == self.upper_bound {
184 self.exhausted = true;
185 }
186 } else {
187 self.exhausted = true;
188 }
189 Ok(rows)
190 }
191
192 pub async fn finish(self) -> Result<(), PageError> {
198 self.transaction
199 .commit()
200 .await
201 .map_err(|source| PageError::sql("finish snapshot transaction", source))
202 }
203
204 pub async fn rollback(self) -> Result<(), PageError> {
209 self.transaction
210 .rollback()
211 .await
212 .map_err(|source| PageError::sql("rollback snapshot transaction", source))
213 }
214
215 pub async fn close(self) -> Result<(), PageError> {
220 self.rollback().await
221 }
222}
223
224async fn query_page_on_connection(
226 connection: &mut PgConnection,
227 tenant_id: Option<&TenantId>,
228 after_row_id: i64,
229 upper_bound: Option<i64>,
230 limit: Limit,
231) -> Result<Vec<PagedEvent>, PageError> {
232 let rows = match upper_bound {
233 Some(upper_bound) => {
234 query_as::<_, PageRow>(SNAPSHOT_PAGE_SQL)
235 .bind(tenant_id.map(TenantId::as_str))
236 .bind(after_row_id)
237 .bind(i64::from(limit.get()))
238 .bind(upper_bound)
239 .fetch_all(&mut *connection)
240 .await
241 }
242 None => {
243 query_as::<_, PageRow>(PAGE_SQL)
244 .bind(tenant_id.map(TenantId::as_str))
245 .bind(after_row_id)
246 .bind(i64::from(limit.get()))
247 .fetch_all(&mut *connection)
248 .await
249 }
250 }
251 .map_err(|source| PageError::sql("read event page", source))?;
252
253 rows.into_iter()
254 .map(hydrate_page)
255 .collect::<Result<Vec<_>, _>>()
256 .map_err(PageError::serialization)
257}
258
259const PAGE_SQL: &str = r"
263 SELECT e.row_id,
264 e.tenant_id,
265 e.stream,
266 e.specversion,
267 e.event_id,
268 e.source,
269 e.event_type,
270 e.subject,
271 e.occurred_at,
272 e.enqueued_at,
273 e.datacontenttype,
274 e.dataschema,
275 e.partitionkey,
276 e.extensions,
277 e.data_kind,
278 e.data,
279 d.state,
280 d.available_at,
281 d.attempts,
282 d.claim_token,
283 d.claimed_by,
284 d.claim_expires_at,
285 d.last_failure_code,
286 d.last_failure_detail,
287 d.delivered_at,
288 d.quarantined_at,
289 d.quarantine_reason
290 FROM dovecote_events AS e
291 LEFT JOIN dovecote_deliveries AS d
292 ON d.tenant_id = e.tenant_id AND d.event_row_id = e.row_id
293 WHERE ($1::varchar IS NULL OR e.tenant_id = $1) AND e.row_id > $2
294 ORDER BY e.row_id ASC
295 LIMIT $3
296";
297
298const SNAPSHOT_PAGE_SQL: &str = r"
299 SELECT e.row_id,
300 e.tenant_id,
301 e.stream,
302 e.specversion,
303 e.event_id,
304 e.source,
305 e.event_type,
306 e.subject,
307 e.occurred_at,
308 e.enqueued_at,
309 e.datacontenttype,
310 e.dataschema,
311 e.partitionkey,
312 e.extensions,
313 e.data_kind,
314 e.data,
315 d.state,
316 d.available_at,
317 d.attempts,
318 d.claim_token,
319 d.claimed_by,
320 d.claim_expires_at,
321 d.last_failure_code,
322 d.last_failure_detail,
323 d.delivered_at,
324 d.quarantined_at,
325 d.quarantine_reason
326 FROM dovecote_events AS e
327 LEFT JOIN dovecote_deliveries AS d
328 ON d.tenant_id = e.tenant_id AND d.event_row_id = e.row_id
329 WHERE ($1::varchar IS NULL OR e.tenant_id = $1) AND e.row_id > $2 AND e.row_id <= $4
330 ORDER BY e.row_id ASC
331 LIMIT $3
332";
333
334#[derive(Debug, FromRow)]
335struct PageRow {
336 row_id: i64,
337 tenant_id: String,
338 stream: String,
339 specversion: String,
340 event_id: String,
341 source: String,
342 event_type: String,
343 subject: Option<String>,
344 occurred_at: Option<OffsetDateTime>,
345 enqueued_at: OffsetDateTime,
346 datacontenttype: Option<String>,
347 dataschema: Option<String>,
348 partitionkey: Option<String>,
349 extensions: String,
350 data_kind: Option<String>,
351 data: Option<Vec<u8>>,
352 state: Option<String>,
353 available_at: Option<OffsetDateTime>,
354 attempts: Option<i64>,
355 claim_token: Option<Vec<u8>>,
356 claimed_by: Option<String>,
357 claim_expires_at: Option<OffsetDateTime>,
358 last_failure_code: Option<String>,
359 last_failure_detail: Option<String>,
360 delivered_at: Option<OffsetDateTime>,
361 quarantined_at: Option<OffsetDateTime>,
362 quarantine_reason: Option<String>,
363}
364
365impl PageRow {
366 fn event_row(&self) -> EventRow<'_> {
367 EventRow {
368 stream: &self.stream,
369 specversion: &self.specversion,
370 event_id: &self.event_id,
371 source: &self.source,
372 event_type: &self.event_type,
373 subject: self.subject.as_deref(),
374 occurred_at: self.occurred_at,
375 datacontenttype: self.datacontenttype.as_deref(),
376 dataschema: self.dataschema.as_deref(),
377 partitionkey: self.partitionkey.as_deref(),
378 extensions: &self.extensions,
379 data_kind: self.data_kind.as_deref(),
380 data: self.data.as_deref(),
381 }
382 }
383}
384
385fn hydrate_page(row: PageRow) -> Result<PagedEvent, String> {
386 let row_id = RowId::new(row.row_id).map_err(|error| error.to_string())?;
387 let tenant_id = TenantId::new(row.tenant_id.clone()).map_err(|error| error.to_string())?;
388 let event = hydrate_event(&row.event_row())?;
389 let state = row
390 .state
391 .ok_or_else(|| format!("event row {} has no required delivery row", row.row_id))?;
392 let available_at = row
393 .available_at
394 .ok_or_else(|| "delivery row has no available_at".to_owned())?;
395 let attempts = AttemptCount::new(
396 row.attempts
397 .ok_or_else(|| "delivery row has no attempts".to_owned())?,
398 )
399 .map_err(|error| error.to_string())?;
400 let failure = parse_failure(row.last_failure_code, row.last_failure_detail)?;
401 let delivery = match state.as_str() {
402 "pending" => {
403 require_absent("pending claim token", row.claim_token.as_ref())?;
404 require_absent("pending claimed worker", row.claimed_by.as_ref())?;
405 require_absent("pending claim expiry", row.claim_expires_at.as_ref())?;
406 require_absent("pending delivered time", row.delivered_at.as_ref())?;
407 require_absent("pending quarantine time", row.quarantined_at.as_ref())?;
408 require_absent("pending quarantine reason", row.quarantine_reason.as_ref())?;
409 DeliverySnapshot::pending(available_at, attempts, failure)
410 }
411 "claimed" => {
412 require_token_width(row.claim_token.as_deref())?;
413 let worker = row
414 .claimed_by
415 .ok_or_else(|| "claimed delivery has no worker".to_owned())?;
416 let expires_at = row
417 .claim_expires_at
418 .ok_or_else(|| "claimed delivery has no claim expiry".to_owned())?;
419 require_absent("claimed delivered time", row.delivered_at.as_ref())?;
420 require_absent("claimed quarantine time", row.quarantined_at.as_ref())?;
421 require_absent("claimed quarantine reason", row.quarantine_reason.as_ref())?;
422 DeliverySnapshot::claimed(
423 available_at,
424 WorkerId::new(worker).map_err(|error| error.to_string())?,
425 expires_at,
426 attempts,
427 failure,
428 )
429 }
430 "delivered" => {
431 require_absent("delivered claim token", row.claim_token.as_ref())?;
432 require_absent("delivered claimed worker", row.claimed_by.as_ref())?;
433 require_absent("delivered claim expiry", row.claim_expires_at.as_ref())?;
434 let delivered_at = row
435 .delivered_at
436 .ok_or_else(|| "delivered delivery has no delivered time".to_owned())?;
437 require_absent("delivered quarantine time", row.quarantined_at.as_ref())?;
438 require_absent(
439 "delivered quarantine reason",
440 row.quarantine_reason.as_ref(),
441 )?;
442 DeliverySnapshot::delivered(available_at, delivered_at, attempts, failure)
443 }
444 "quarantined" => {
445 require_absent("quarantined claim token", row.claim_token.as_ref())?;
446 require_absent("quarantined claimed worker", row.claimed_by.as_ref())?;
447 require_absent("quarantined claim expiry", row.claim_expires_at.as_ref())?;
448 require_absent("quarantined delivered time", row.delivered_at.as_ref())?;
449 let quarantined_at = row
450 .quarantined_at
451 .ok_or_else(|| "quarantined delivery has no quarantine time".to_owned())?;
452 let reason = row
453 .quarantine_reason
454 .ok_or_else(|| "quarantined delivery has no quarantine reason".to_owned())?;
455 DeliverySnapshot::quarantined(
456 available_at,
457 quarantined_at,
458 attempts,
459 failure,
460 QuarantineReason::new(reason).map_err(|error| error.to_string())?,
461 )
462 }
463 state => return Err(format!("unknown delivery state {state:?}")),
464 }
465 .map_err(|error| error.to_string())?;
466
467 PagedEvent::new(tenant_id, row_id, event, row.enqueued_at, delivery)
468 .map_err(|error| error.to_string())
469}
470
471fn require_absent<T>(field: &str, value: Option<&T>) -> Result<(), String> {
472 if value.is_some() {
473 Err(format!("{field} must be NULL for its delivery state"))
474 } else {
475 Ok(())
476 }
477}
478
479fn require_token_width(value: Option<&[u8]>) -> Result<(), String> {
480 match value {
481 Some(value) if value.len() == dovecote::CLAIM_TOKEN_BYTES => Ok(()),
482 Some(value) => Err(format!(
483 "claimed delivery has an invalid claim token width: {}",
484 value.len()
485 )),
486 None => Err("claimed delivery has no claim token".to_owned()),
487 }
488}
489
490fn parse_failure(code: Option<String>, detail: Option<String>) -> Result<Option<Failure>, String> {
491 match (code, detail) {
492 (None, None) => Ok(None),
493 (Some(code), Some(detail)) => Failure::new(code, detail)
494 .map(Some)
495 .map_err(|error| error.to_string()),
496 _ => Err("delivery failure code and detail must be both NULL or non-NULL".to_owned()),
497 }
498}