mod redis_storage;
pub use redis_storage::RedisDlqStorage;
use crate::error::{MeiliBridgeError, Result};
use crate::models::event::Event;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};
use tracing::{error, info, warn};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeadLetterEntry {
pub id: String,
pub task_id: String,
pub event: Event,
pub error: String,
pub retry_count: u32,
pub created_at: DateTime<Utc>,
pub last_retry_at: Option<DateTime<Utc>>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DlqStatistics {
pub total_entries: usize,
pub entries_by_task: HashMap<String, usize>,
pub entries_by_error: HashMap<String, usize>,
pub oldest_entry: Option<DateTime<Utc>>,
pub newest_entry: Option<DateTime<Utc>>,
}
#[async_trait::async_trait]
pub trait DlqStorage: Send + Sync {
async fn store(&self, entry: DeadLetterEntry) -> Result<()>;
async fn get_by_task(
&self,
task_id: &str,
limit: Option<usize>,
) -> Result<Vec<DeadLetterEntry>>;
async fn get(&self, id: &str) -> Result<Option<DeadLetterEntry>>;
async fn remove(&self, id: &str) -> Result<()>;
async fn update(&self, entry: DeadLetterEntry) -> Result<()>;
async fn get_statistics(&self) -> Result<DlqStatistics>;
async fn clear_task(&self, task_id: &str) -> Result<usize>;
}
pub struct InMemoryDlqStorage {
entries: Arc<RwLock<HashMap<String, DeadLetterEntry>>>,
}
impl InMemoryDlqStorage {
pub fn new() -> Self {
Self {
entries: Arc::new(RwLock::new(HashMap::new())),
}
}
}
impl Default for InMemoryDlqStorage {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
impl DlqStorage for InMemoryDlqStorage {
async fn store(&self, entry: DeadLetterEntry) -> Result<()> {
let mut entries = self.entries.write().await;
entries.insert(entry.id.clone(), entry);
Ok(())
}
async fn get_by_task(
&self,
task_id: &str,
limit: Option<usize>,
) -> Result<Vec<DeadLetterEntry>> {
let entries = self.entries.read().await;
let mut task_entries: Vec<_> = entries
.values()
.filter(|e| e.task_id == task_id)
.cloned()
.collect();
task_entries.sort_by(|a, b| a.created_at.cmp(&b.created_at));
if let Some(limit) = limit {
task_entries.truncate(limit);
}
Ok(task_entries)
}
async fn get(&self, id: &str) -> Result<Option<DeadLetterEntry>> {
let entries = self.entries.read().await;
Ok(entries.get(id).cloned())
}
async fn remove(&self, id: &str) -> Result<()> {
let mut entries = self.entries.write().await;
entries.remove(id);
Ok(())
}
async fn update(&self, entry: DeadLetterEntry) -> Result<()> {
let mut entries = self.entries.write().await;
entries.insert(entry.id.clone(), entry);
Ok(())
}
async fn get_statistics(&self) -> Result<DlqStatistics> {
let entries = self.entries.read().await;
let mut entries_by_task = HashMap::new();
let mut entries_by_error = HashMap::new();
let mut oldest: Option<DateTime<Utc>> = None;
let mut newest: Option<DateTime<Utc>> = None;
for entry in entries.values() {
*entries_by_task.entry(entry.task_id.clone()).or_insert(0) += 1;
let error_type = entry.error.lines().next().unwrap_or("Unknown").to_string();
*entries_by_error.entry(error_type).or_insert(0) += 1;
match oldest {
None => oldest = Some(entry.created_at),
Some(old) if entry.created_at < old => oldest = Some(entry.created_at),
_ => {}
}
match newest {
None => newest = Some(entry.created_at),
Some(new) if entry.created_at > new => newest = Some(entry.created_at),
_ => {}
}
}
Ok(DlqStatistics {
total_entries: entries.len(),
entries_by_task,
entries_by_error,
oldest_entry: oldest,
newest_entry: newest,
})
}
async fn clear_task(&self, task_id: &str) -> Result<usize> {
let mut entries = self.entries.write().await;
let before_count = entries.len();
entries.retain(|_, entry| entry.task_id != task_id);
let removed_count = before_count - entries.len();
Ok(removed_count)
}
}
pub struct DeadLetterQueue {
storage: Arc<dyn DlqStorage>,
reprocess_tx: Option<mpsc::Sender<DeadLetterEntry>>,
}
impl DeadLetterQueue {
pub fn new(storage: Arc<dyn DlqStorage>) -> Self {
Self {
storage,
reprocess_tx: None,
}
}
pub fn set_reprocess_channel(&mut self, tx: mpsc::Sender<DeadLetterEntry>) {
self.reprocess_tx = Some(tx);
}
pub async fn add_failed_event(
&self,
task_id: String,
event: Event,
error: MeiliBridgeError,
retry_count: u32,
) -> Result<()> {
let entry = DeadLetterEntry {
id: uuid::Uuid::new_v4().to_string(),
task_id,
event,
error: error.to_string(),
retry_count,
created_at: Utc::now(),
last_retry_at: if retry_count > 0 {
Some(Utc::now())
} else {
None
},
metadata: HashMap::new(),
};
let task_id = entry.task_id.clone();
warn!(
"Adding event to dead letter queue. Task: {}, Error: {}, Retries: {}",
entry.task_id, entry.error, entry.retry_count
);
self.storage.store(entry).await?;
crate::metrics::DEAD_LETTER_EVENTS_TOTAL
.with_label_values(&[&task_id])
.inc();
Ok(())
}
pub async fn reprocess_entries(&self, task_id: &str, limit: Option<usize>) -> Result<usize> {
let entries = self.storage.get_by_task(task_id, limit).await?;
let count = entries.len();
if count == 0 {
info!("No dead letter entries found for task: {}", task_id);
return Ok(0);
}
let reprocess_tx = self.reprocess_tx.as_ref().ok_or_else(|| {
MeiliBridgeError::Configuration("Reprocess channel not configured".to_string())
})?;
info!(
"Reprocessing {} dead letter entries for task: {}",
count, task_id
);
for mut entry in entries {
entry.retry_count += 1;
entry.last_retry_at = Some(Utc::now());
self.storage.update(entry.clone()).await?;
if let Err(e) = reprocess_tx.send(entry.clone()).await {
error!("Failed to send entry for reprocessing: {}", e);
return Err(MeiliBridgeError::Pipeline(format!(
"Failed to reprocess entry: {}",
e
)));
}
self.storage.remove(&entry.id).await?;
}
info!("Successfully queued {} entries for reprocessing", count);
Ok(count)
}
pub async fn get_statistics(&self) -> Result<DlqStatistics> {
self.storage.get_statistics().await
}
pub async fn clear_task(&self, task_id: &str) -> Result<usize> {
let count = self.storage.clear_task(task_id).await?;
info!(
"Cleared {} dead letter entries for task: {}",
count, task_id
);
Ok(count)
}
}