use std::collections::HashSet;
use std::sync::RwLock;
use super::ChangeEvent;
pub struct ExactlyOnceDedup {
processed_txids: RwLock<HashSet<String>>,
capacity: usize,
}
impl ExactlyOnceDedup {
pub fn new() -> Self {
Self::with_capacity(100_000)
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
processed_txids: RwLock::new(HashSet::new()),
capacity,
}
}
pub fn check_and_mark(&self, event: &ChangeEvent) -> bool {
let mut txids = self.processed_txids.write().expect("txids lock poisoned");
if txids.len() >= self.capacity {
txids.clear();
}
txids.insert(event.transaction_id.clone())
}
pub fn is_duplicate(&self, event: &ChangeEvent) -> bool {
let txids = self.processed_txids.read().expect("txids lock poisoned");
txids.contains(&event.transaction_id)
}
pub fn mark_processed(&self, txid: &str) {
let mut txids = self.processed_txids.write().expect("txids lock poisoned");
if txids.len() >= self.capacity {
txids.clear();
}
txids.insert(txid.to_string());
}
pub fn processed_count(&self) -> usize {
self.processed_txids
.read()
.expect("txids lock poisoned")
.len()
}
pub fn clear(&self) {
let mut txids = self.processed_txids.write().expect("txids lock poisoned");
txids.clear();
}
}
impl Default for ExactlyOnceDedup {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cdc::ChangeOp;
use std::collections::HashMap;
fn make_event(txid: &str) -> ChangeEvent {
ChangeEvent {
op: ChangeOp::Insert,
before: None,
after: Some(HashMap::new()),
timestamp: 0,
transaction_id: txid.to_string(),
table: "users".to_string(),
schema: "public".to_string(),
}
}
#[test]
fn test_dedup_first_event_not_duplicate() {
let dedup = ExactlyOnceDedup::new();
let event = make_event("tx-001");
assert!(dedup.check_and_mark(&event));
assert!(dedup.is_duplicate(&event));
assert!(!dedup.check_and_mark(&event));
}
#[test]
fn test_dedup_duplicate_event() {
let dedup = ExactlyOnceDedup::new();
let event = make_event("tx-001");
dedup.check_and_mark(&event);
assert!(!dedup.check_and_mark(&event));
assert!(dedup.is_duplicate(&event));
}
#[test]
fn test_dedup_different_txids() {
let dedup = ExactlyOnceDedup::new();
let e1 = make_event("tx-001");
let e2 = make_event("tx-002");
assert!(dedup.check_and_mark(&e1));
assert!(dedup.check_and_mark(&e2));
assert_eq!(dedup.processed_count(), 2);
}
#[test]
fn test_dedup_capacity_eviction() {
let dedup = ExactlyOnceDedup::with_capacity(2);
let e1 = make_event("tx-001");
let e2 = make_event("tx-002");
let e3 = make_event("tx-003");
dedup.check_and_mark(&e1);
dedup.check_and_mark(&e2);
dedup.check_and_mark(&e3);
assert_eq!(dedup.processed_count(), 1);
}
#[test]
fn test_dedup_clear() {
let dedup = ExactlyOnceDedup::new();
let event = make_event("tx-001");
dedup.check_and_mark(&event);
assert_eq!(dedup.processed_count(), 1);
dedup.clear();
assert_eq!(dedup.processed_count(), 0);
}
#[test]
fn test_dedup_mark_processed() {
let dedup = ExactlyOnceDedup::new();
dedup.mark_processed("tx-001");
let event = make_event("tx-001");
assert!(dedup.is_duplicate(&event));
}
}