use crate::error::{Mecha10Error, Result};
use crate::messages::Message;
use crate::prelude::now_micros;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, error, info, warn};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FailedMessage<T> {
pub message: T,
pub failed_at: u64,
pub source: String,
pub error: String,
pub retry_count: u32,
pub metadata: HashMap<String, String>,
pub stack_trace: Option<String>,
}
impl<T: Message + Serialize> Message for FailedMessage<T> where FailedMessage<T>: Send + Sync + 'static {}
#[derive(Debug, Clone)]
pub struct DeadLetterConfig {
pub max_size: usize,
pub persist: bool,
pub persist_path: Option<String>,
pub publish_to_topic: bool,
pub topic_prefix: String,
}
impl Default for DeadLetterConfig {
fn default() -> Self {
Self {
max_size: 1000,
persist: false,
persist_path: None,
publish_to_topic: true,
topic_prefix: "/dlq".to_string(),
}
}
}
#[derive(Clone)]
pub struct DeadLetterQueue {
name: String,
config: DeadLetterConfig,
messages: Arc<RwLock<Vec<serde_json::Value>>>,
stats: Arc<RwLock<DLQStats>>,
}
#[derive(Debug, Clone, Default)]
struct DLQStats {
total_failed: u64,
total_recovered: u64,
total_dropped: u64,
}
impl DeadLetterQueue {
pub fn new(name: &str) -> Self {
Self::with_config(name, DeadLetterConfig::default())
}
pub fn with_config(name: &str, config: DeadLetterConfig) -> Self {
info!(
"Creating dead letter queue '{}' (max_size: {}, persist: {})",
name, config.max_size, config.persist
);
Self {
name: name.to_string(),
config,
messages: Arc::new(RwLock::new(Vec::new())),
stats: Arc::new(RwLock::new(DLQStats::default())),
}
}
pub async fn send<T: Message + Serialize + Clone>(
&self,
message: &T,
source: &str,
error: &str,
retry_count: Option<u32>,
) -> Result<()> {
self.send_with_metadata(message, source, error, retry_count, HashMap::new())
.await
}
pub async fn send_with_metadata<T: Message + Serialize + Clone>(
&self,
message: &T,
source: &str,
error: &str,
retry_count: Option<u32>,
metadata: HashMap<String, String>,
) -> Result<()> {
let failed_msg = FailedMessage {
message: message.clone(),
failed_at: now_micros(),
source: source.to_string(),
error: error.to_string(),
retry_count: retry_count.unwrap_or(0),
metadata,
stack_trace: None, };
{
let mut stats = self.stats.write().await;
stats.total_failed += 1;
}
let json = serde_json::to_value(&failed_msg)
.map_err(|e| Mecha10Error::SerializationFailed(format!("Failed to serialize DLQ message: {}", e)))?;
{
let mut messages = self.messages.write().await;
if messages.len() >= self.config.max_size {
warn!(
"DLQ '{}' at capacity ({}), dropping oldest message",
self.name, self.config.max_size
);
messages.remove(0);
let mut stats = self.stats.write().await;
stats.total_dropped += 1;
}
messages.push(json.clone());
}
error!(
"Message sent to DLQ '{}' from '{}': {} (retry_count: {})",
self.name,
source,
error,
retry_count.unwrap_or(0)
);
if self.config.persist {
self.persist_message(&json).await?;
}
if self.config.publish_to_topic {
self.publish_to_dlq_topic(failed_msg.clone(), source).await?;
}
Ok(())
}
pub async fn get_all(&self) -> Vec<serde_json::Value> {
let messages = self.messages.read().await;
messages.clone()
}
pub async fn get_by_source(&self, source: &str) -> Vec<serde_json::Value> {
let messages = self.messages.read().await;
messages
.iter()
.filter(|msg| {
msg.get("source")
.and_then(|s| s.as_str())
.map(|s| s == source)
.unwrap_or(false)
})
.cloned()
.collect()
}
pub async fn len(&self) -> usize {
let messages = self.messages.read().await;
messages.len()
}
pub async fn is_empty(&self) -> bool {
self.len().await == 0
}
pub async fn clear(&self) {
let mut messages = self.messages.write().await;
let count = messages.len();
messages.clear();
info!("Cleared {} messages from DLQ '{}'", count, self.name);
}
pub async fn remove(&self, index: usize) -> Option<serde_json::Value> {
let mut messages = self.messages.write().await;
if index < messages.len() {
let msg = messages.remove(index);
let mut stats = self.stats.write().await;
stats.total_recovered += 1;
debug!("Removed message {} from DLQ '{}'", index, self.name);
Some(msg)
} else {
None
}
}
pub async fn stats(&self) -> (u64, u64, u64, usize) {
let stats = self.stats.read().await;
let current = self.len().await;
(stats.total_failed, stats.total_recovered, stats.total_dropped, current)
}
async fn persist_message(&self, message: &serde_json::Value) -> Result<()> {
if let Some(path) = &self.config.persist_path {
let json_str = serde_json::to_string(message)
.map_err(|e| Mecha10Error::SerializationFailed(format!("Failed to serialize: {}", e)))?;
tokio::fs::write(path, json_str).await?;
debug!("Persisted DLQ message to {}", path);
}
Ok(())
}
async fn publish_to_dlq_topic<T: Message + Serialize + Clone>(
&self,
_failed_msg: FailedMessage<T>,
source: &str,
) -> Result<()> {
debug!(
"Would publish to DLQ topic: {}/{}/{}",
self.config.topic_prefix, self.name, source
);
Ok(())
}
}
impl std::fmt::Debug for DeadLetterQueue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DeadLetterQueue")
.field("name", &self.name)
.field("config", &self.config)
.finish()
}
}
pub trait DeadLetterExt<T> {
fn process_with_dlq<F, Fut, R>(
self,
dlq: &DeadLetterQueue,
source: &str,
processor: F,
) -> impl std::future::Future<Output = Result<()>>
where
F: Fn(T) -> Fut,
Fut: std::future::Future<Output = Result<R>>;
}