Skip to main content

llm_optimizer_processor/deduplication/
mod.rs

1//! Event Deduplication System
2//!
3//! This module provides enterprise-grade event deduplication capabilities with Redis backend
4//! support, configurable event ID extraction, TTL-based cleanup, and comprehensive metrics.
5//!
6//! ## Features
7//!
8//! - **Multiple ID Extraction Strategies**: UUID, hash-based, custom extractors
9//! - **Redis-Backed Storage**: Production-ready distributed deduplication
10//! - **TTL-Based Cleanup**: Automatic memory management with configurable expiration
11//! - **Batch Operations**: Efficient bulk deduplication checks
12//! - **Retry Logic**: Exponential backoff for transient failures
13//! - **Comprehensive Metrics**: Track duplicates, cache hits, and performance
14//! - **Thread-Safe**: Lock-free concurrent operations with Arc/DashMap
15//! - **Zero Data Loss**: Guaranteed at-most-once processing semantics
16//!
17//! ## Architecture
18//!
19//! The deduplication system uses a layered architecture:
20//!
21//! ```text
22//! ┌─────────────────────────────────┐
23//! │   EventDeduplicator (API)       │
24//! └─────────────┬───────────────────┘
25//!               │
26//! ┌─────────────┴───────────────────┐
27//! │   EventIdExtractor (Strategy)   │
28//! └─────────────┬───────────────────┘
29//!               │
30//! ┌─────────────┴───────────────────┐
31//! │ RedisDeduplicationBackend       │
32//! │  - Connection pooling           │
33//! │  - Retry logic                  │
34//! │  - Metrics tracking             │
35//! └─────────────────────────────────┘
36//! ```
37//!
38//! ## Usage Example
39//!
40//! ```rust,no_run
41//! use processor::deduplication::{
42//!     EventDeduplicator, DeduplicationConfig, EventIdExtractor,
43//! };
44//! use serde::{Deserialize, Serialize};
45//! use std::time::Duration;
46//!
47//! #[derive(Debug, Clone, Serialize, Deserialize)]
48//! struct MyEvent {
49//!     id: String,
50//!     data: String,
51//! }
52//!
53//! #[tokio::main]
54//! async fn main() -> anyhow::Result<()> {
55//!     // Configure deduplicator
56//!     let config = DeduplicationConfig::builder()
57//!         .redis_url("redis://localhost:6379")
58//!         .key_prefix("dedup:")
59//!         .default_ttl(Duration::from_secs(3600))
60//!         .build()?;
61//!
62//!     // Create custom ID extractor
63//!     let extractor = EventIdExtractor::custom(|event: &MyEvent| {
64//!         event.id.clone()
65//!     });
66//!
67//!     // Initialize deduplicator
68//!     let dedup = EventDeduplicator::new(config, extractor).await?;
69//!
70//!     // Check and process events
71//!     let event = MyEvent {
72//!         id: "event-123".to_string(),
73//!         data: "payload".to_string(),
74//!     };
75//!
76//!     if !dedup.is_duplicate(&event).await? {
77//!         // Process event
78//!         println!("Processing new event: {}", event.id);
79//!
80//!         // Mark as seen
81//!         dedup.mark_seen(&event).await?;
82//!     } else {
83//!         println!("Duplicate event detected: {}", event.id);
84//!     }
85//!
86//!     // Get statistics
87//!     let stats = dedup.stats().await;
88//!     println!("Duplicate rate: {:.2}%", stats.duplicate_rate() * 100.0);
89//!
90//!     Ok(())
91//! }
92//! ```
93//!
94//! ## Batch Processing
95//!
96//! For high-throughput scenarios, use batch operations:
97//!
98//! ```rust,no_run
99//! # use processor::deduplication::{EventDeduplicator, DeduplicationConfig, EventIdExtractor};
100//! # use serde::{Deserialize, Serialize};
101//! # #[derive(Debug, Clone, Serialize, Deserialize)]
102//! # struct MyEvent { id: String }
103//! # async fn example(dedup: EventDeduplicator<MyEvent>) -> anyhow::Result<()> {
104//! let events = vec![
105//!     MyEvent { id: "event-1".to_string() },
106//!     MyEvent { id: "event-2".to_string() },
107//!     MyEvent { id: "event-3".to_string() },
108//! ];
109//!
110//! // Check all events in one batch
111//! let results = dedup.batch_check_duplicates(&events).await?;
112//!
113//! for (event, is_duplicate) in events.iter().zip(results.iter()) {
114//!     if !is_duplicate {
115//!         println!("Processing: {}", event.id);
116//!     }
117//! }
118//!
119//! // Mark all non-duplicates as seen
120//! let new_events: Vec<_> = events.iter()
121//!     .zip(results.iter())
122//!     .filter_map(|(e, dup)| if !dup { Some(e) } else { None })
123//!     .collect();
124//!
125//! dedup.batch_mark_seen(&new_events).await?;
126//! # Ok(())
127//! # }
128//! ```
129
130use async_trait::async_trait;
131use redis::{aio::ConnectionManager, AsyncCommands, Client, RedisResult};
132use serde::{Deserialize, Serialize};
133use std::fmt::Debug;
134use std::hash::{Hash, Hasher};
135use std::marker::PhantomData;
136use std::sync::Arc;
137use std::time::Duration;
138use tokio::sync::RwLock;
139use tracing::{debug, error, trace, warn};
140use uuid::Uuid;
141
142use crate::error::{StateError, StateResult};
143
144/// Configuration for event deduplication
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct DeduplicationConfig {
147    /// Redis connection URL
148    pub redis_url: String,
149    /// Key prefix for namespacing deduplication keys
150    pub key_prefix: String,
151    /// Default TTL for deduplication entries (None = no expiration)
152    pub default_ttl: Option<Duration>,
153    /// Connection timeout
154    pub connect_timeout: Duration,
155    /// Read timeout
156    pub read_timeout: Duration,
157    /// Write timeout
158    pub write_timeout: Duration,
159    /// Maximum retry attempts for failed operations
160    pub max_retries: u32,
161    /// Base delay for exponential backoff
162    pub retry_base_delay: Duration,
163    /// Maximum delay between retries
164    pub retry_max_delay: Duration,
165    /// Pipeline batch size for bulk operations
166    pub pipeline_batch_size: usize,
167    /// Enable compression for event IDs (saves space for large IDs)
168    pub compress_event_ids: bool,
169}
170
171impl Default for DeduplicationConfig {
172    fn default() -> Self {
173        Self {
174            redis_url: "redis://localhost:6379".to_string(),
175            key_prefix: "dedup:".to_string(),
176            default_ttl: Some(Duration::from_secs(3600)), // 1 hour default
177            connect_timeout: Duration::from_secs(5),
178            read_timeout: Duration::from_secs(3),
179            write_timeout: Duration::from_secs(3),
180            max_retries: 3,
181            retry_base_delay: Duration::from_millis(100),
182            retry_max_delay: Duration::from_secs(5),
183            pipeline_batch_size: 100,
184            compress_event_ids: false,
185        }
186    }
187}
188
189impl DeduplicationConfig {
190    /// Create a new configuration builder
191    pub fn builder() -> DeduplicationConfigBuilder {
192        DeduplicationConfigBuilder::default()
193    }
194}
195
196/// Builder for DeduplicationConfig
197#[derive(Debug, Default)]
198pub struct DeduplicationConfigBuilder {
199    config: DeduplicationConfig,
200}
201
202impl DeduplicationConfigBuilder {
203    /// Set Redis URL
204    pub fn redis_url(mut self, url: impl Into<String>) -> Self {
205        self.config.redis_url = url.into();
206        self
207    }
208
209    /// Set key prefix for namespace isolation
210    pub fn key_prefix(mut self, prefix: impl Into<String>) -> Self {
211        self.config.key_prefix = prefix.into();
212        self
213    }
214
215    /// Set default TTL for deduplication entries
216    pub fn default_ttl(mut self, ttl: Duration) -> Self {
217        self.config.default_ttl = Some(ttl);
218        self
219    }
220
221    /// Set connection timeout
222    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
223        self.config.connect_timeout = timeout;
224        self
225    }
226
227    /// Set retry configuration (max attempts, base delay, max delay)
228    pub fn retries(mut self, max: u32, base_delay: Duration, max_delay: Duration) -> Self {
229        self.config.max_retries = max;
230        self.config.retry_base_delay = base_delay;
231        self.config.retry_max_delay = max_delay;
232        self
233    }
234
235    /// Set pipeline batch size
236    pub fn pipeline_batch_size(mut self, size: usize) -> Self {
237        self.config.pipeline_batch_size = size;
238        self
239    }
240
241    /// Enable event ID compression
242    pub fn compress_event_ids(mut self, enabled: bool) -> Self {
243        self.config.compress_event_ids = enabled;
244        self
245    }
246
247    /// Build the configuration
248    pub fn build(self) -> StateResult<DeduplicationConfig> {
249        if self.config.redis_url.is_empty() {
250            return Err(StateError::StorageError {
251                backend_type: "redis-dedup".to_string(),
252                details: "Redis URL cannot be empty".to_string(),
253            });
254        }
255
256        if self.config.pipeline_batch_size == 0 {
257            return Err(StateError::StorageError {
258                backend_type: "redis-dedup".to_string(),
259                details: "Pipeline batch size must be greater than 0".to_string(),
260            });
261        }
262
263        Ok(self.config)
264    }
265}
266
267/// Statistics for deduplication operations
268#[derive(Debug, Clone, Default, Serialize, Deserialize)]
269pub struct DeduplicationStats {
270    /// Total events checked
271    pub events_checked: u64,
272    /// Total duplicates found
273    pub duplicates_found: u64,
274    /// Total new events (marked as seen)
275    pub new_events: u64,
276    /// Total cache hits (event found in Redis)
277    pub cache_hits: u64,
278    /// Total cache misses (event not in Redis)
279    pub cache_misses: u64,
280    /// Total batch operations
281    pub batch_operations: u64,
282    /// Total retry attempts
283    pub retry_count: u64,
284    /// Total failed operations
285    pub error_count: u64,
286    /// Average operation latency (microseconds)
287    pub avg_latency_us: u64,
288    /// Total bytes written (event IDs)
289    pub bytes_written: u64,
290    /// Total bytes read
291    pub bytes_read: u64,
292}
293
294impl DeduplicationStats {
295    /// Calculate duplicate rate (0.0 to 1.0)
296    pub fn duplicate_rate(&self) -> f64 {
297        if self.events_checked == 0 {
298            0.0
299        } else {
300            self.duplicates_found as f64 / self.events_checked as f64
301        }
302    }
303
304    /// Calculate cache hit rate (0.0 to 1.0)
305    pub fn cache_hit_rate(&self) -> f64 {
306        let total = self.cache_hits + self.cache_misses;
307        if total == 0 {
308            0.0
309        } else {
310            self.cache_hits as f64 / total as f64
311        }
312    }
313
314    /// Calculate error rate (0.0 to 1.0)
315    pub fn error_rate(&self) -> f64 {
316        if self.events_checked == 0 {
317            0.0
318        } else {
319            self.error_count as f64 / self.events_checked as f64
320        }
321    }
322
323    /// Get total operations count
324    pub fn total_operations(&self) -> u64 {
325        self.events_checked + self.new_events
326    }
327}
328
329/// Strategy for extracting unique event IDs
330pub enum EventIdExtractor<T> {
331    /// Extract UUID from event
332    Uuid(Box<dyn Fn(&T) -> Uuid + Send + Sync>),
333    /// Extract string ID from event
334    String(Box<dyn Fn(&T) -> String + Send + Sync>),
335    /// Generate hash-based ID from event
336    Hash(Box<dyn Fn(&T) -> u64 + Send + Sync>),
337    /// Custom extraction logic
338    Custom(Box<dyn Fn(&T) -> String + Send + Sync>),
339}
340
341impl<T> EventIdExtractor<T> {
342    /// Create UUID-based extractor
343    pub fn uuid<F>(f: F) -> Self
344    where
345        F: Fn(&T) -> Uuid + Send + Sync + 'static,
346    {
347        Self::Uuid(Box::new(f))
348    }
349
350    /// Create string-based extractor
351    pub fn string<F>(f: F) -> Self
352    where
353        F: Fn(&T) -> String + Send + Sync + 'static,
354    {
355        Self::String(Box::new(f))
356    }
357
358    /// Create hash-based extractor
359    pub fn hash<F>(f: F) -> Self
360    where
361        F: Fn(&T) -> u64 + Send + Sync + 'static,
362    {
363        Self::Hash(Box::new(f))
364    }
365
366    /// Create custom string extractor
367    pub fn custom<F>(f: F) -> Self
368    where
369        F: Fn(&T) -> String + Send + Sync + 'static,
370    {
371        Self::Custom(Box::new(f))
372    }
373
374    /// Extract event ID as a string
375    pub fn extract_id(&self, event: &T) -> String {
376        match self {
377            Self::Uuid(f) => f(event).to_string(),
378            Self::String(f) => f(event),
379            Self::Hash(f) => format!("{:016x}", f(event)),
380            Self::Custom(f) => f(event),
381        }
382    }
383}
384
385impl<T> Debug for EventIdExtractor<T> {
386    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387        match self {
388            Self::Uuid(_) => write!(f, "EventIdExtractor::Uuid"),
389            Self::String(_) => write!(f, "EventIdExtractor::String"),
390            Self::Hash(_) => write!(f, "EventIdExtractor::Hash"),
391            Self::Custom(_) => write!(f, "EventIdExtractor::Custom"),
392        }
393    }
394}
395
396/// Redis-backed deduplication backend
397struct RedisDeduplicationBackend {
398    /// Redis connection manager
399    connection: Arc<ConnectionManager>,
400    /// Configuration
401    config: DeduplicationConfig,
402    /// Statistics tracking
403    stats: Arc<RwLock<DeduplicationStats>>,
404}
405
406impl RedisDeduplicationBackend {
407    /// Create a new Redis deduplication backend
408    async fn new(config: DeduplicationConfig) -> StateResult<Self> {
409        let client = Self::create_client(&config)?;
410        let connection = Self::create_connection_manager(client, &config).await?;
411
412        Ok(Self {
413            connection: Arc::new(connection),
414            config,
415            stats: Arc::new(RwLock::new(DeduplicationStats::default())),
416        })
417    }
418
419    /// Create Redis client from configuration
420    fn create_client(config: &DeduplicationConfig) -> StateResult<Client> {
421        Client::open(config.redis_url.as_str()).map_err(|e| StateError::StorageError {
422            backend_type: "redis-dedup".to_string(),
423            details: format!("Failed to create Redis client: {}", e),
424        })
425    }
426
427    /// Create connection manager with retry logic
428    async fn create_connection_manager(
429        client: Client,
430        config: &DeduplicationConfig,
431    ) -> StateResult<ConnectionManager> {
432        let mut retries = 0;
433        let mut delay = config.retry_base_delay;
434
435        loop {
436            match ConnectionManager::new(client.clone()).await {
437                Ok(conn) => {
438                    debug!("Redis deduplication backend connected");
439                    return Ok(conn);
440                }
441                Err(e) => {
442                    retries += 1;
443                    if retries >= config.max_retries {
444                        error!("Failed to connect to Redis after {} retries: {}", retries, e);
445                        return Err(StateError::StorageError {
446                            backend_type: "redis-dedup".to_string(),
447                            details: format!("Connection failed after {} retries: {}", retries, e),
448                        });
449                    }
450
451                    warn!(
452                        "Redis connection attempt {} failed: {}, retrying in {:?}",
453                        retries, e, delay
454                    );
455                    tokio::time::sleep(delay).await;
456                    delay = std::cmp::min(delay * 2, config.retry_max_delay);
457                }
458            }
459        }
460    }
461
462    /// Build full key with prefix
463    fn build_key(&self, event_id: &str) -> String {
464        format!("{}{}", self.config.key_prefix, event_id)
465    }
466
467    /// Execute operation with retry logic
468    async fn with_retry<F, Fut, T>(&self, mut operation: F) -> StateResult<T>
469    where
470        F: FnMut() -> Fut,
471        Fut: std::future::Future<Output = RedisResult<T>>,
472    {
473        let mut retries = 0;
474        let mut delay = self.config.retry_base_delay;
475
476        loop {
477            match operation().await {
478                Ok(result) => return Ok(result),
479                Err(e) => {
480                    retries += 1;
481
482                    // Update error stats
483                    let mut stats = self.stats.write().await;
484                    stats.error_count += 1;
485                    stats.retry_count += 1;
486                    drop(stats);
487
488                    if retries >= self.config.max_retries {
489                        error!("Deduplication operation failed after {} retries: {}", retries, e);
490                        return Err(StateError::StorageError {
491                            backend_type: "redis-dedup".to_string(),
492                            details: format!("Operation failed after {} retries: {}", retries, e),
493                        });
494                    }
495
496                    warn!(
497                        "Deduplication operation attempt {} failed: {}, retrying in {:?}",
498                        retries, e, delay
499                    );
500                    tokio::time::sleep(delay).await;
501                    delay = std::cmp::min(delay * 2, self.config.retry_max_delay);
502                }
503            }
504        }
505    }
506
507    /// Check if event ID exists (is a duplicate)
508    async fn exists(&self, event_id: &str) -> StateResult<bool> {
509        let start = std::time::Instant::now();
510        let key = self.build_key(event_id);
511        trace!("Checking existence of key: {}", key);
512
513        let conn = self.connection.as_ref().clone();
514        let key_clone = key.clone();
515
516        let exists: bool = self
517            .with_retry(move || {
518                let mut conn = conn.clone();
519                let key = key_clone.clone();
520                async move { conn.exists(&key).await }
521            })
522            .await?;
523
524        // Update stats
525        let mut stats = self.stats.write().await;
526        stats.events_checked += 1;
527        if exists {
528            stats.duplicates_found += 1;
529            stats.cache_hits += 1;
530        } else {
531            stats.cache_misses += 1;
532        }
533
534        // Update latency
535        let latency_us = start.elapsed().as_micros() as u64;
536        stats.avg_latency_us = if stats.events_checked == 1 {
537            latency_us
538        } else {
539            (stats.avg_latency_us * 9 + latency_us) / 10
540        };
541
542        Ok(exists)
543    }
544
545    /// Mark event ID as seen (add to deduplication store)
546    async fn mark_seen(&self, event_id: &str) -> StateResult<()> {
547        let key = self.build_key(event_id);
548        trace!("Marking key as seen: {}", key);
549
550        let conn = self.connection.as_ref().clone();
551        let key_clone = key.clone();
552        let ttl = self.config.default_ttl;
553
554        self.with_retry(move || {
555            let mut conn = conn.clone();
556            let key = key_clone.clone();
557            async move {
558                if let Some(ttl_duration) = ttl {
559                    let ttl_secs = ttl_duration.as_secs();
560                    conn.set_ex(&key, "1", ttl_secs).await
561                } else {
562                    conn.set(&key, "1").await
563                }
564            }
565        })
566        .await?;
567
568        // Update stats
569        let mut stats = self.stats.write().await;
570        stats.new_events += 1;
571        stats.bytes_written += event_id.len() as u64;
572
573        Ok(())
574    }
575
576    /// Batch check multiple event IDs
577    async fn batch_exists(&self, event_ids: &[String]) -> StateResult<Vec<bool>> {
578        if event_ids.is_empty() {
579            return Ok(Vec::new());
580        }
581
582        let keys: Vec<String> = event_ids.iter().map(|id| self.build_key(id)).collect();
583        let conn = self.connection.as_ref().clone();
584        let keys_clone = keys.clone();
585
586        let results: Vec<bool> = self
587            .with_retry(move || {
588                let mut conn = conn.clone();
589                let keys = keys_clone.clone();
590                async move {
591                    let mut pipe = redis::pipe();
592                    for key in &keys {
593                        pipe.exists(key);
594                    }
595                    pipe.query_async(&mut conn).await
596                }
597            })
598            .await?;
599
600        // Update stats
601        let mut stats = self.stats.write().await;
602        stats.events_checked += event_ids.len() as u64;
603        stats.batch_operations += 1;
604
605        for &exists in &results {
606            if exists {
607                stats.duplicates_found += 1;
608                stats.cache_hits += 1;
609            } else {
610                stats.cache_misses += 1;
611            }
612        }
613
614        Ok(results)
615    }
616
617    /// Batch mark multiple event IDs as seen
618    async fn batch_mark_seen(&self, event_ids: &[String]) -> StateResult<()> {
619        if event_ids.is_empty() {
620            return Ok(());
621        }
622
623        // Process in chunks based on pipeline batch size
624        for chunk in event_ids.chunks(self.config.pipeline_batch_size) {
625            let commands: Vec<(String, Option<u64>)> = chunk
626                .iter()
627                .map(|id| {
628                    let key = self.build_key(id);
629                    let ttl = self.config.default_ttl.map(|d| d.as_secs());
630                    (key, ttl)
631                })
632                .collect();
633
634            let conn = self.connection.as_ref().clone();
635
636            self.with_retry(move || {
637                let mut conn = conn.clone();
638                let commands = commands.clone();
639                async move {
640                    let mut pipe = redis::pipe();
641                    for (key, ttl) in &commands {
642                        if let Some(ttl_secs) = ttl {
643                            pipe.set_ex(key, "1", *ttl_secs);
644                        } else {
645                            pipe.set(key, "1");
646                        }
647                    }
648                    pipe.query_async(&mut conn).await
649                }
650            })
651            .await?;
652
653            // Update stats
654            let mut stats = self.stats.write().await;
655            stats.new_events += chunk.len() as u64;
656            stats.batch_operations += 1;
657            stats.bytes_written += chunk.iter().map(|id| id.len() as u64).sum::<u64>();
658        }
659
660        Ok(())
661    }
662
663    /// Get current statistics
664    async fn stats(&self) -> DeduplicationStats {
665        self.stats.read().await.clone()
666    }
667
668    /// Check connection health
669    async fn health_check(&self) -> StateResult<bool> {
670        let mut conn = self.connection.as_ref().clone();
671        let result: RedisResult<String> = redis::cmd("PING").query_async(&mut conn).await;
672
673        match result {
674            Ok(response) => Ok(response == "PONG"),
675            Err(e) => {
676                error!("Deduplication backend health check failed: {}", e);
677                Err(StateError::StorageError {
678                    backend_type: "redis-dedup".to_string(),
679                    details: format!("Health check failed: {}", e),
680                })
681            }
682        }
683    }
684
685    /// Clear all deduplication data (use with caution!)
686    async fn clear(&self) -> StateResult<usize> {
687        let pattern = format!("{}*", self.config.key_prefix);
688        let mut conn = self.connection.as_ref().clone();
689        let mut cursor = 0u64;
690        let mut total_deleted = 0;
691
692        loop {
693            let (new_cursor, keys): (u64, Vec<String>) = redis::cmd("SCAN")
694                .arg(cursor)
695                .arg("MATCH")
696                .arg(&pattern)
697                .arg("COUNT")
698                .arg(1000)
699                .query_async(&mut conn)
700                .await
701                .map_err(|e| StateError::StorageError {
702                    backend_type: "redis-dedup".to_string(),
703                    details: format!("SCAN failed: {}", e),
704                })?;
705
706            if !keys.is_empty() {
707                let deleted: usize = redis::cmd("DEL")
708                    .arg(&keys)
709                    .query_async(&mut conn)
710                    .await
711                    .map_err(|e| StateError::StorageError {
712                        backend_type: "redis-dedup".to_string(),
713                        details: format!("DEL failed: {}", e),
714                    })?;
715                total_deleted += deleted;
716            }
717
718            cursor = new_cursor;
719            if cursor == 0 {
720                break;
721            }
722        }
723
724        debug!("Cleared {} deduplication entries", total_deleted);
725        Ok(total_deleted)
726    }
727}
728
729/// High-level event deduplicator
730///
731/// Provides a type-safe interface for event deduplication with configurable
732/// ID extraction strategies and comprehensive error handling.
733pub struct EventDeduplicator<T> {
734    /// Backend storage
735    backend: Arc<RedisDeduplicationBackend>,
736    /// Event ID extraction strategy
737    extractor: EventIdExtractor<T>,
738    /// Phantom data for type safety
739    _phantom: PhantomData<T>,
740}
741
742impl<T> EventDeduplicator<T> {
743    /// Create a new event deduplicator
744    ///
745    /// # Arguments
746    ///
747    /// * `config` - Deduplication configuration
748    /// * `extractor` - Event ID extraction strategy
749    ///
750    /// # Returns
751    ///
752    /// A new `EventDeduplicator` instance
753    ///
754    /// # Errors
755    ///
756    /// Returns error if Redis connection fails
757    pub async fn new(
758        config: DeduplicationConfig,
759        extractor: EventIdExtractor<T>,
760    ) -> StateResult<Self> {
761        let backend = RedisDeduplicationBackend::new(config).await?;
762
763        Ok(Self {
764            backend: Arc::new(backend),
765            extractor,
766            _phantom: PhantomData,
767        })
768    }
769
770    /// Check if an event is a duplicate
771    ///
772    /// # Arguments
773    ///
774    /// * `event` - The event to check
775    ///
776    /// # Returns
777    ///
778    /// `true` if the event is a duplicate, `false` otherwise
779    ///
780    /// # Errors
781    ///
782    /// Returns error if Redis operation fails
783    pub async fn is_duplicate(&self, event: &T) -> StateResult<bool> {
784        let event_id = self.extractor.extract_id(event);
785        self.backend.exists(&event_id).await
786    }
787
788    /// Mark an event as seen
789    ///
790    /// # Arguments
791    ///
792    /// * `event` - The event to mark as seen
793    ///
794    /// # Errors
795    ///
796    /// Returns error if Redis operation fails
797    pub async fn mark_seen(&self, event: &T) -> StateResult<()> {
798        let event_id = self.extractor.extract_id(event);
799        self.backend.mark_seen(&event_id).await
800    }
801
802    /// Check and mark event as seen in one operation (optimized)
803    ///
804    /// # Arguments
805    ///
806    /// * `event` - The event to check and mark
807    ///
808    /// # Returns
809    ///
810    /// `true` if the event was a duplicate, `false` if it was new
811    ///
812    /// # Errors
813    ///
814    /// Returns error if Redis operation fails
815    pub async fn check_and_mark(&self, event: &T) -> StateResult<bool> {
816        let event_id = self.extractor.extract_id(event);
817        let was_duplicate = self.backend.exists(&event_id).await?;
818
819        if !was_duplicate {
820            self.backend.mark_seen(&event_id).await?;
821        }
822
823        Ok(was_duplicate)
824    }
825
826    /// Batch check multiple events for duplicates
827    ///
828    /// # Arguments
829    ///
830    /// * `events` - Slice of events to check
831    ///
832    /// # Returns
833    ///
834    /// Vector of booleans indicating which events are duplicates
835    ///
836    /// # Errors
837    ///
838    /// Returns error if Redis operation fails
839    pub async fn batch_check_duplicates(&self, events: &[T]) -> StateResult<Vec<bool>> {
840        let event_ids: Vec<String> = events.iter().map(|e| self.extractor.extract_id(e)).collect();
841        self.backend.batch_exists(&event_ids).await
842    }
843
844    /// Batch mark multiple events as seen
845    ///
846    /// # Arguments
847    ///
848    /// * `events` - Slice of events to mark as seen
849    ///
850    /// # Errors
851    ///
852    /// Returns error if Redis operation fails
853    pub async fn batch_mark_seen(&self, events: &[&T]) -> StateResult<()> {
854        let event_ids: Vec<String> = events.iter().map(|e| self.extractor.extract_id(e)).collect();
855        self.backend.batch_mark_seen(&event_ids).await
856    }
857
858    /// Get deduplication statistics
859    pub async fn stats(&self) -> DeduplicationStats {
860        self.backend.stats().await
861    }
862
863    /// Check backend health
864    pub async fn health_check(&self) -> StateResult<bool> {
865        self.backend.health_check().await
866    }
867
868    /// Clear all deduplication data (use with caution!)
869    ///
870    /// This removes all deduplication state. Only use for testing or maintenance.
871    pub async fn clear(&self) -> StateResult<usize> {
872        self.backend.clear().await
873    }
874}
875
876impl<T> Clone for EventDeduplicator<T> {
877    fn clone(&self) -> Self {
878        Self {
879            backend: Arc::clone(&self.backend),
880            // Note: We cannot clone the extractor as it contains Box<dyn Fn>
881            // Users should create a new instance with the same extractor
882            extractor: EventIdExtractor::Custom(Box::new(|_| {
883                panic!("Cannot clone EventDeduplicator with extractor. Create a new instance instead.")
884            })),
885            _phantom: PhantomData,
886        }
887    }
888}
889
890impl<T> Debug for EventDeduplicator<T> {
891    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
892        f.debug_struct("EventDeduplicator")
893            .field("extractor", &self.extractor)
894            .finish()
895    }
896}
897
898#[cfg(test)]
899mod tests {
900    use super::*;
901    use serde::{Deserialize, Serialize};
902
903    #[derive(Debug, Clone, Serialize, Deserialize)]
904    struct TestEvent {
905        id: String,
906        data: String,
907    }
908
909    // Helper to create test deduplicator (requires Redis running)
910    async fn create_test_deduplicator() -> Option<EventDeduplicator<TestEvent>> {
911        let config = DeduplicationConfig::builder()
912            .redis_url("redis://localhost:6379")
913            .key_prefix("test:dedup:")
914            .default_ttl(Duration::from_secs(60))
915            .build()
916            .ok()?;
917
918        let extractor = EventIdExtractor::string(|event: &TestEvent| event.id.clone());
919
920        match EventDeduplicator::new(config, extractor).await {
921            Ok(dedup) => {
922                // Clear any existing test data
923                let _ = dedup.clear().await;
924                Some(dedup)
925            }
926            Err(_) => None,
927        }
928    }
929
930    #[tokio::test]
931    async fn test_basic_deduplication() {
932        if let Some(dedup) = create_test_deduplicator().await {
933            let event1 = TestEvent {
934                id: "event-1".to_string(),
935                data: "data1".to_string(),
936            };
937
938            // First check - should not be duplicate
939            assert!(!dedup.is_duplicate(&event1).await.unwrap());
940
941            // Mark as seen
942            dedup.mark_seen(&event1).await.unwrap();
943
944            // Second check - should be duplicate
945            assert!(dedup.is_duplicate(&event1).await.unwrap());
946        } else {
947            eprintln!("Skipping test: Redis not available");
948        }
949    }
950
951    #[tokio::test]
952    async fn test_check_and_mark() {
953        if let Some(dedup) = create_test_deduplicator().await {
954            let event = TestEvent {
955                id: "event-check-mark".to_string(),
956                data: "data".to_string(),
957            };
958
959            // First call - should not be duplicate and mark it
960            let was_dup1 = dedup.check_and_mark(&event).await.unwrap();
961            assert!(!was_dup1);
962
963            // Second call - should be duplicate
964            let was_dup2 = dedup.check_and_mark(&event).await.unwrap();
965            assert!(was_dup2);
966        } else {
967            eprintln!("Skipping test: Redis not available");
968        }
969    }
970
971    #[tokio::test]
972    async fn test_batch_operations() {
973        if let Some(dedup) = create_test_deduplicator().await {
974            let events = vec![
975                TestEvent {
976                    id: "batch-1".to_string(),
977                    data: "data1".to_string(),
978                },
979                TestEvent {
980                    id: "batch-2".to_string(),
981                    data: "data2".to_string(),
982                },
983                TestEvent {
984                    id: "batch-3".to_string(),
985                    data: "data3".to_string(),
986                },
987            ];
988
989            // Check all - should be new
990            let results = dedup.batch_check_duplicates(&events).await.unwrap();
991            assert_eq!(results, vec![false, false, false]);
992
993            // Mark all as seen
994            let refs: Vec<&TestEvent> = events.iter().collect();
995            dedup.batch_mark_seen(&refs).await.unwrap();
996
997            // Check again - should be duplicates
998            let results2 = dedup.batch_check_duplicates(&events).await.unwrap();
999            assert_eq!(results2, vec![true, true, true]);
1000        } else {
1001            eprintln!("Skipping test: Redis not available");
1002        }
1003    }
1004
1005    #[tokio::test]
1006    async fn test_stats() {
1007        if let Some(dedup) = create_test_deduplicator().await {
1008            let event1 = TestEvent {
1009                id: "stats-1".to_string(),
1010                data: "data1".to_string(),
1011            };
1012            let event2 = TestEvent {
1013                id: "stats-2".to_string(),
1014                data: "data2".to_string(),
1015            };
1016
1017            // Check first event (miss)
1018            dedup.is_duplicate(&event1).await.unwrap();
1019            // Mark it
1020            dedup.mark_seen(&event1).await.unwrap();
1021            // Check again (hit, duplicate)
1022            dedup.is_duplicate(&event1).await.unwrap();
1023            // Check new event (miss)
1024            dedup.is_duplicate(&event2).await.unwrap();
1025
1026            let stats = dedup.stats().await;
1027            assert_eq!(stats.events_checked, 3);
1028            assert_eq!(stats.duplicates_found, 1);
1029            assert_eq!(stats.new_events, 1);
1030            assert_eq!(stats.cache_hits, 1);
1031            assert_eq!(stats.cache_misses, 2);
1032            assert!(stats.duplicate_rate() > 0.0);
1033        } else {
1034            eprintln!("Skipping test: Redis not available");
1035        }
1036    }
1037
1038    #[tokio::test]
1039    async fn test_uuid_extractor() {
1040        if let Some(_) = create_test_deduplicator().await {
1041            #[derive(Debug, Clone)]
1042            struct UuidEvent {
1043                uuid: Uuid,
1044            }
1045
1046            let config = DeduplicationConfig::builder()
1047                .redis_url("redis://localhost:6379")
1048                .key_prefix("test:uuid:")
1049                .build()
1050                .unwrap();
1051
1052            let extractor = EventIdExtractor::uuid(|event: &UuidEvent| event.uuid);
1053
1054            if let Ok(dedup) = EventDeduplicator::new(config, extractor).await {
1055                let uuid = Uuid::new_v4();
1056                let event = UuidEvent { uuid };
1057
1058                assert!(!dedup.is_duplicate(&event).await.unwrap());
1059                dedup.mark_seen(&event).await.unwrap();
1060                assert!(dedup.is_duplicate(&event).await.unwrap());
1061
1062                dedup.clear().await.unwrap();
1063            }
1064        } else {
1065            eprintln!("Skipping test: Redis not available");
1066        }
1067    }
1068
1069    #[tokio::test]
1070    async fn test_hash_extractor() {
1071        if let Some(_) = create_test_deduplicator().await {
1072            use std::collections::hash_map::DefaultHasher;
1073
1074            #[derive(Debug, Clone)]
1075            struct HashEvent {
1076                data: String,
1077            }
1078
1079            let config = DeduplicationConfig::builder()
1080                .redis_url("redis://localhost:6379")
1081                .key_prefix("test:hash:")
1082                .build()
1083                .unwrap();
1084
1085            let extractor = EventIdExtractor::hash(|event: &HashEvent| {
1086                let mut hasher = DefaultHasher::new();
1087                event.data.hash(&mut hasher);
1088                hasher.finish()
1089            });
1090
1091            if let Ok(dedup) = EventDeduplicator::new(config, extractor).await {
1092                let event1 = HashEvent {
1093                    data: "same-data".to_string(),
1094                };
1095                let event2 = HashEvent {
1096                    data: "same-data".to_string(),
1097                };
1098
1099                assert!(!dedup.is_duplicate(&event1).await.unwrap());
1100                dedup.mark_seen(&event1).await.unwrap();
1101                // event2 has same data, so same hash
1102                assert!(dedup.is_duplicate(&event2).await.unwrap());
1103
1104                dedup.clear().await.unwrap();
1105            }
1106        } else {
1107            eprintln!("Skipping test: Redis not available");
1108        }
1109    }
1110
1111    #[tokio::test]
1112    async fn test_health_check() {
1113        if let Some(dedup) = create_test_deduplicator().await {
1114            let healthy = dedup.health_check().await.unwrap();
1115            assert!(healthy);
1116        } else {
1117            eprintln!("Skipping test: Redis not available");
1118        }
1119    }
1120
1121    #[tokio::test]
1122    async fn test_clear() {
1123        if let Some(dedup) = create_test_deduplicator().await {
1124            let events: Vec<TestEvent> = (0..5)
1125                .map(|i| TestEvent {
1126                    id: format!("clear-{}", i),
1127                    data: "data".to_string(),
1128                })
1129                .collect();
1130
1131            // Mark all as seen
1132            let refs: Vec<&TestEvent> = events.iter().collect();
1133            dedup.batch_mark_seen(&refs).await.unwrap();
1134
1135            // Clear all
1136            let deleted = dedup.clear().await.unwrap();
1137            assert_eq!(deleted, 5);
1138
1139            // Check all - should not be duplicates
1140            let results = dedup.batch_check_duplicates(&events).await.unwrap();
1141            assert_eq!(results, vec![false, false, false, false, false]);
1142        } else {
1143            eprintln!("Skipping test: Redis not available");
1144        }
1145    }
1146
1147    #[test]
1148    fn test_config_builder() {
1149        let config = DeduplicationConfig::builder()
1150            .redis_url("redis://localhost:6379")
1151            .key_prefix("myapp:dedup:")
1152            .default_ttl(Duration::from_secs(7200))
1153            .pipeline_batch_size(200)
1154            .compress_event_ids(true)
1155            .build()
1156            .unwrap();
1157
1158        assert_eq!(config.redis_url, "redis://localhost:6379");
1159        assert_eq!(config.key_prefix, "myapp:dedup:");
1160        assert_eq!(config.default_ttl, Some(Duration::from_secs(7200)));
1161        assert_eq!(config.pipeline_batch_size, 200);
1162        assert!(config.compress_event_ids);
1163    }
1164
1165    #[test]
1166    fn test_stats_calculations() {
1167        let mut stats = DeduplicationStats::default();
1168        assert_eq!(stats.duplicate_rate(), 0.0);
1169        assert_eq!(stats.cache_hit_rate(), 0.0);
1170
1171        stats.events_checked = 100;
1172        stats.duplicates_found = 25;
1173        stats.cache_hits = 25;
1174        stats.cache_misses = 75;
1175
1176        assert_eq!(stats.duplicate_rate(), 0.25);
1177        assert_eq!(stats.cache_hit_rate(), 0.25);
1178        assert_eq!(stats.total_operations(), 100);
1179    }
1180}