use super::*;
struct SqlDedupStore {
pool: AnyPool,
driver_name: String,
table: String,
ttl_seconds: u64,
last_cleanup: Arc<std::sync::atomic::AtomicU64>,
}
impl SqlDedupStore {
async fn ensure_table(&self) -> anyhow::Result<()> {
let sql = format!(
"CREATE TABLE IF NOT EXISTS {} (dedup_key VARCHAR(255) PRIMARY KEY, expire_at BIGINT NOT NULL)",
self.table
);
sqlx::query(audited_sql(&sql))
.execute(&self.pool)
.await
.with_context(|| format!("Failed to create dedup table '{}'", self.table))?;
let idx = format!(
"CREATE INDEX IF NOT EXISTS {0}_expire_idx ON {0} (expire_at)",
self.table
);
let _ = sqlx::query(audited_sql(&idx)).execute(&self.pool).await;
Ok(())
}
}
#[async_trait]
impl crate::middleware::deduplication::DedupStore for SqlDedupStore {
async fn reserve(&self, key: &[u8], now: u64) -> Result<bool, ConsumerError> {
let id = crate::middleware::deduplication::hex_key(key);
let pending_expiry = (now + crate::middleware::deduplication::PENDING_TTL_SECS) as i64;
let del = format!(
"DELETE FROM {} WHERE dedup_key = {} AND expire_at <= {}",
self.table,
positional_placeholder(&self.driver_name, 1),
positional_placeholder(&self.driver_name, 2)
);
sqlx::query(audited_sql(&del))
.bind(id.clone())
.bind(now as i64)
.execute(&self.pool)
.await
.map_err(|e| ConsumerError::Connection(anyhow!("dedup SQL delete failed: {e}")))?;
let ins = format!(
"INSERT INTO {} (dedup_key, expire_at) VALUES ({}, {})",
self.table,
positional_placeholder(&self.driver_name, 1),
positional_placeholder(&self.driver_name, 2)
);
match sqlx::query(audited_sql(&ins))
.bind(id)
.bind(pending_expiry)
.execute(&self.pool)
.await
{
Ok(_) => Ok(false),
Err(e) => {
if e.as_database_error()
.map(|d| d.is_unique_violation())
.unwrap_or(false)
{
Ok(true)
} else {
Err(ConsumerError::Connection(anyhow!(
"dedup SQL reserve failed: {e}"
)))
}
}
}
}
async fn mark_processed(&self, key: &[u8], now: u64) {
let id = crate::middleware::deduplication::hex_key(key);
let expire_at = (now + self.ttl_seconds) as i64;
let sql = format!(
"UPDATE {} SET expire_at = {} WHERE dedup_key = {}",
self.table,
positional_placeholder(&self.driver_name, 1),
positional_placeholder(&self.driver_name, 2)
);
if let Err(e) = sqlx::query(audited_sql(&sql))
.bind(expire_at)
.bind(id)
.execute(&self.pool)
.await
{
warn!("Failed to mark dedup key processed in SQL: {}", e);
}
}
fn maybe_cleanup(&self, now: u64) {
use std::sync::atomic::Ordering;
let last = self.last_cleanup.load(Ordering::Acquire);
if now.saturating_sub(last) > 30
&& self
.last_cleanup
.compare_exchange(last, now, Ordering::SeqCst, Ordering::Acquire)
.is_ok()
{
let pool = self.pool.clone();
let driver = self.driver_name.clone();
let table = self.table.clone();
tokio::spawn(async move {
let sql = format!(
"DELETE FROM {} WHERE expire_at < {}",
table,
positional_placeholder(&driver, 1)
);
if let Err(e) = sqlx::query(audited_sql(&sql))
.bind(now as i64)
.execute(&pool)
.await
{
warn!("dedup SQL cleanup failed: {}", e);
}
});
}
}
}
pub(crate) async fn build_sql_dedup_store(
url: &str,
table: Option<String>,
ttl_seconds: u64,
route_name: &str,
) -> anyhow::Result<Arc<dyn crate::middleware::deduplication::DedupStore>> {
sqlx::any::install_default_drivers();
let pool = AnyPool::connect(url)
.await
.with_context(|| format!("Failed to connect deduplication store at '{}'", url))?;
let driver_name = {
let conn = pool.acquire().await?;
let name = conn.backend_name().to_string();
drop(conn);
name
};
let table = table.unwrap_or_else(|| {
format!(
"mqb_dedup_{}",
crate::checkpoint::sanitize_ident(route_name)
)
});
if !is_valid_table_name(&table) {
return Err(anyhow!("Invalid deduplication table name: '{}'.", table));
}
let store = SqlDedupStore {
pool,
driver_name,
table,
ttl_seconds,
last_cleanup: Arc::new(std::sync::atomic::AtomicU64::new(0)),
};
store.ensure_table().await?;
Ok(Arc::new(store))
}