use super::{
circuit_breaker::{CircuitBreaker, CircuitBreakerState},
config::DeduplicationConfig,
error::{DeduplicationError, DeduplicationResult},
extractors::EventIdExtractor,
id::EventId,
stats::{DeduplicationStats, HealthState, HealthStatus},
strategy::DeduplicationStrategy,
EventDeduplicator,
};
use crate::state::RedisStateBackend;
use async_trait::async_trait;
use std::marker::PhantomData;
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::RwLock;
use tracing::{debug, error, info, trace, warn};
pub struct RedisEventDeduplicator<T, E> {
redis: Arc<RedisStateBackend>,
id_extractor: Arc<E>,
config: DeduplicationConfig,
stats: Arc<RwLock<DeduplicationStats>>,
circuit_breaker: Arc<CircuitBreaker>,
_phantom: PhantomData<T>,
}
impl<T, E> RedisEventDeduplicator<T, E>
where
T: Send + Sync,
E: EventIdExtractor<T> + Send + Sync,
{
pub async fn new(
redis: RedisStateBackend,
id_extractor: E,
config: DeduplicationConfig,
) -> DeduplicationResult<Self> {
config.validate().map_err(|e| DeduplicationError::InvalidConfig {
reason: e.to_string(),
})?;
let circuit_breaker = CircuitBreaker::new(
config.circuit_breaker_threshold,
config.circuit_breaker_timeout,
);
info!(
strategy = %config.strategy,
ttl_secs = config.ttl.as_secs(),
"Redis event deduplicator initialized"
);
Ok(Self {
redis: Arc::new(redis),
id_extractor: Arc::new(id_extractor),
config,
stats: Arc::new(RwLock::new(DeduplicationStats::default())),
circuit_breaker: Arc::new(circuit_breaker),
_phantom: PhantomData,
})
}
fn build_dedup_key(&self, event_id: &EventId) -> Vec<u8> {
format!("{}{}", self.config.key_prefix, event_id.as_str()).into_bytes()
}
async fn extract_event_id(&self, event: &T) -> DeduplicationResult<EventId> {
match self.id_extractor.extract_id(event) {
Ok(id) => {
trace!(event_id = %id, "Event ID extracted");
Ok(id)
}
Err(e) => {
error!(error = %e, "Failed to extract event ID");
let mut stats = self.stats.write().await;
stats.extraction_failures += 1;
match self.config.strategy {
DeduplicationStrategy::Strict => Err(e),
_ => {
warn!("Allowing event due to extraction failure");
stats.fallback_count += 1;
Err(e) }
}
}
}
}
async fn check_duplicate_internal(&self, event_id: &EventId) -> DeduplicationResult<bool> {
if self.circuit_breaker.is_open().await {
return self.handle_circuit_breaker_open();
}
let key = self.build_dedup_key(event_id);
trace!(event_id = %event_id, "Checking duplicate in Redis");
match self
.redis
.set_if_not_exists(&key, b"1", self.config.ttl)
.await
{
Ok(was_set) => {
self.circuit_breaker.record_success().await;
let is_duplicate = !was_set;
if is_duplicate {
debug!(event_id = %event_id, "Duplicate event detected");
} else {
trace!(event_id = %event_id, "Unique event marked");
}
Ok(is_duplicate)
}
Err(e) => {
error!(error = %e, event_id = %event_id, "Redis operation failed");
self.circuit_breaker.record_failure().await;
let mut stats = self.stats.write().await;
stats.redis_errors += 1;
stats.last_error_time = Some(chrono::Utc::now());
match self.config.strategy {
DeduplicationStrategy::Strict => Err(e.into()),
_ => {
warn!(
strategy = %self.config.strategy,
"Redis unavailable, treating event as unique"
);
stats.fallback_count += 1;
Ok(false) }
}
}
}
}
fn handle_circuit_breaker_open(&self) -> DeduplicationResult<bool> {
warn!("Circuit breaker is open");
match self.config.strategy {
DeduplicationStrategy::Strict => Err(DeduplicationError::CircuitBreakerOpen),
_ => {
info!("Allowing event through due to circuit breaker");
Ok(false) }
}
}
async fn update_latency(&self, duration: std::time::Duration) {
let mut stats = self.stats.write().await;
let latency_us = duration.as_micros() as u64;
stats.total_latency_us += latency_us;
if stats.total_checked > 0 {
stats.avg_latency_us = stats.total_latency_us / stats.total_checked;
}
if latency_us > stats.p99_latency_us {
stats.p99_latency_us = latency_us;
}
if latency_us > stats.p95_latency_us {
stats.p95_latency_us = latency_us;
}
if latency_us > stats.p50_latency_us || stats.p50_latency_us == 0 {
stats.p50_latency_us = latency_us;
}
}
}
#[async_trait]
impl<T, E> EventDeduplicator for RedisEventDeduplicator<T, E>
where
T: Send + Sync,
E: EventIdExtractor<T> + Send + Sync,
{
async fn is_duplicate<U>(&self, event: &U) -> DeduplicationResult<bool>
where
U: Send + Sync,
{
Err(DeduplicationError::ExtractionFailed {
reason: "Type mismatch - use check_and_mark with correct event type".to_string(),
})
}
async fn mark_seen<U>(&self, event: &U) -> DeduplicationResult<()>
where
U: Send + Sync,
{
Err(DeduplicationError::ExtractionFailed {
reason: "Type mismatch - use check_and_mark with correct event type".to_string(),
})
}
async fn check_and_mark<U>(&self, event: &U) -> DeduplicationResult<bool>
where
U: Send + Sync,
{
Err(DeduplicationError::ExtractionFailed {
reason: "Type mismatch - use the typed check_and_mark_event method".to_string(),
})
}
async fn stats(&self) -> DeduplicationStats {
self.stats.read().await.clone()
}
async fn health_check(&self) -> DeduplicationResult<HealthStatus> {
let redis_available = self.redis.health_check().await.unwrap_or(false);
let cb_state = self.circuit_breaker.state().await;
let stats = self.stats.read().await.clone();
let mut messages = Vec::new();
let status = match (redis_available, cb_state) {
(true, CircuitBreakerState::Closed) => {
if stats.error_rate() < 0.05 {
HealthState::Healthy
} else {
messages.push(format!(
"High error rate: {:.1}%",
stats.error_rate() * 100.0
));
HealthState::Degraded
}
}
(true, CircuitBreakerState::HalfOpen) => {
messages.push("Circuit breaker in half-open state".to_string());
HealthState::Degraded
}
(false, _) | (_, CircuitBreakerState::Open) => {
if !redis_available {
messages.push("Redis is unavailable".to_string());
}
if matches!(cb_state, CircuitBreakerState::Open) {
messages.push("Circuit breaker is open".to_string());
}
HealthState::Unhealthy
}
};
Ok(HealthStatus {
status,
redis_available,
circuit_breaker_state: cb_state,
last_check_time: stats.last_check_time,
last_error_time: stats.last_error_time,
stats,
messages,
})
}
async fn clear(&self) -> DeduplicationResult<()> {
warn!("Clearing all deduplication state");
let mut stats = self.stats.write().await;
*stats = DeduplicationStats::default();
self.circuit_breaker.reset().await;
Ok(())
}
}
impl<T, E> RedisEventDeduplicator<T, E>
where
T: Send + Sync,
E: EventIdExtractor<T> + Send + Sync,
{
pub async fn is_duplicate_event(&self, event: &T) -> DeduplicationResult<bool> {
let event_id = self.extract_event_id(event).await?;
let key = self.build_dedup_key(&event_id);
match self.redis.contains(&key).await {
Ok(exists) => {
self.circuit_breaker.record_success().await;
Ok(exists)
}
Err(e) => {
self.circuit_breaker.record_failure().await;
match self.config.strategy {
DeduplicationStrategy::Strict => Err(e.into()),
_ => Ok(false),
}
}
}
}
pub async fn mark_seen_event(&self, event: &T) -> DeduplicationResult<()> {
let event_id = self.extract_event_id(event).await?;
let key = self.build_dedup_key(&event_id);
match self.redis.put_with_ttl(&key, b"1", self.config.ttl).await {
Ok(()) => {
self.circuit_breaker.record_success().await;
Ok(())
}
Err(e) => {
self.circuit_breaker.record_failure().await;
match self.config.strategy {
DeduplicationStrategy::Strict => Err(e.into()),
_ => Ok(()),
}
}
}
}
pub async fn check_and_mark_event(&self, event: &T) -> DeduplicationResult<bool> {
let start = Instant::now();
let event_id = match self.extract_event_id(event).await {
Ok(id) => id,
Err(_) => {
return match self.config.strategy {
DeduplicationStrategy::Strict => Err(DeduplicationError::ExtractionFailed {
reason: "Failed to extract event ID".to_string(),
}),
_ => Ok(false), };
}
};
let is_duplicate = self.check_duplicate_internal(&event_id).await?;
let latency = start.elapsed();
let mut stats = self.stats.write().await;
stats.total_checked += 1;
stats.last_check_time = Some(chrono::Utc::now());
stats.circuit_breaker_state = self.circuit_breaker.state().await;
if is_duplicate {
stats.duplicates_found += 1;
if self.config.strategy.should_filter() {
debug!(event_id = %event_id, "Filtering duplicate event");
} else {
info!(event_id = %event_id, "Duplicate detected (log-only mode)");
}
} else {
stats.unique_events += 1;
}
drop(stats); self.update_latency(latency).await;
if !self.config.strategy.should_filter() {
Ok(false)
} else {
Ok(is_duplicate)
}
}
pub async fn check_and_mark_batch(&self, events: &[T]) -> DeduplicationResult<Vec<bool>> {
let mut results = Vec::with_capacity(events.len());
for event in events {
let is_dup = self.check_and_mark_event(event).await?;
results.push(is_dup);
}
Ok(results)
}
pub async fn filter_batch(&self, events: Vec<T>) -> DeduplicationResult<Vec<T>>
where
T: Clone,
{
let mut unique = Vec::new();
for event in events {
let is_dup = self.check_and_mark_event(&event).await?;
if !is_dup {
unique.push(event);
}
}
Ok(unique)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::deduplication::extractors::FeedbackEventUuidExtractor;
use crate::state::RedisConfig;
use llm_optimizer_types::events::{EventSource, EventType, FeedbackEvent};
use std::time::Duration;
async fn create_test_deduplicator(
) -> Option<RedisEventDeduplicator<FeedbackEvent, FeedbackEventUuidExtractor>> {
let redis_config = RedisConfig::builder()
.url("redis://localhost:6379")
.key_prefix("test:dedup:")
.default_ttl(Duration::from_secs(60))
.build()
.ok()?;
let redis = RedisStateBackend::new(redis_config).await.ok()?;
let config = DeduplicationConfig::builder()
.strategy(DeduplicationStrategy::BestEffort)
.ttl(Duration::from_secs(60))
.key_prefix("dedup:".to_string())
.build()
.ok()?;
let deduplicator =
RedisEventDeduplicator::new(redis, FeedbackEventUuidExtractor, config)
.await
.ok()?;
let _ = deduplicator.clear().await;
Some(deduplicator)
}
#[tokio::test]
async fn test_deduplicator_basic() {
let Some(dedup) = create_test_deduplicator().await else {
eprintln!("Skipping test: Redis not available");
return;
};
let event = FeedbackEvent::new(
EventSource::Observatory,
EventType::Metric,
serde_json::json!({"test": "data"}),
);
let is_dup1 = dedup.check_and_mark_event(&event).await.unwrap();
assert!(!is_dup1);
let is_dup2 = dedup.check_and_mark_event(&event).await.unwrap();
assert!(is_dup2);
let stats = dedup.stats().await;
assert_eq!(stats.total_checked, 2);
assert_eq!(stats.unique_events, 1);
assert_eq!(stats.duplicates_found, 1);
}
#[tokio::test]
async fn test_health_check() {
let Some(dedup) = create_test_deduplicator().await else {
eprintln!("Skipping test: Redis not available");
return;
};
let health = dedup.health_check().await.unwrap();
assert!(health.redis_available);
assert_eq!(health.circuit_breaker_state, CircuitBreakerState::Closed);
}
}