cratestack_sqlx/
idempotency.rs1mod operations;
13
14use std::time::SystemTime;
15
16use async_trait::async_trait;
17use cratestack_axum::idempotency::{IDEMPOTENCY_TABLE_DDL, IdempotencyStore, ReservationOutcome};
18use cratestack_core::CoolError;
19
20use crate::sqlx;
21
22#[derive(Clone)]
23pub struct SqlxIdempotencyStore {
24 pool: sqlx::PgPool,
25}
26
27impl SqlxIdempotencyStore {
28 pub fn new(pool: sqlx::PgPool) -> Self {
29 Self { pool }
30 }
31
32 pub async fn ensure_schema(&self) -> Result<(), CoolError> {
35 sqlx::raw_sql(IDEMPOTENCY_TABLE_DDL)
39 .execute(&self.pool)
40 .await
41 .map_err(|error| CoolError::Database(error.to_string()))?;
42 Ok(())
43 }
44
45 pub async fn garbage_collect(&self) -> Result<u64, CoolError> {
49 let result = sqlx::query("DELETE FROM cratestack_idempotency WHERE expires_at < NOW()")
50 .execute(&self.pool)
51 .await
52 .map_err(|error| CoolError::Database(error.to_string()))?;
53 Ok(result.rows_affected())
54 }
55}
56
57#[async_trait]
58impl IdempotencyStore for SqlxIdempotencyStore {
59 async fn reserve_or_fetch(
60 &self,
61 principal: &str,
62 key: &str,
63 request_hash: [u8; 32],
64 expires_at: SystemTime,
65 ) -> Result<ReservationOutcome, CoolError> {
66 operations::reserve_or_fetch(&self.pool, principal, key, request_hash, expires_at).await
67 }
68
69 async fn complete(
70 &self,
71 principal: &str,
72 key: &str,
73 token: uuid::Uuid,
74 status: u16,
75 headers: &[u8],
76 body: &[u8],
77 ) -> Result<(), CoolError> {
78 operations::complete(&self.pool, principal, key, token, status, headers, body).await
79 }
80
81 async fn release(
82 &self,
83 principal: &str,
84 key: &str,
85 token: uuid::Uuid,
86 ) -> Result<(), CoolError> {
87 operations::release(&self.pool, principal, key, token).await
88 }
89}
90
91pub fn expiry_from(created_at: SystemTime, ttl: std::time::Duration) -> SystemTime {
96 created_at + ttl
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use std::time::{Duration, SystemTime};
103
104 #[test]
105 fn expiry_adds_ttl_to_creation() {
106 let now = SystemTime::UNIX_EPOCH;
107 let expiry = expiry_from(now, Duration::from_secs(24 * 3600));
108 assert_eq!(
109 expiry.duration_since(SystemTime::UNIX_EPOCH).unwrap(),
110 Duration::from_secs(24 * 3600),
111 );
112 }
113}