use crate::common::model::config::RedisConfig;
use crate::common::model::{
Request, Response,
message::{TaskErrorEvent, TaskEvent, TaskParserEvent},
};
use crate::errors::Result;
use crate::errors::error::QueueError;
use crate::utils::logger::LogModel;
use async_trait::async_trait;
use deadpool_redis::redis;
use log::{error, warn};
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio::time::Duration;
pub trait Identifiable {
fn get_id(&self) -> String;
fn partition_key(&self) -> String {
self.get_id()
}
}
impl Identifiable for LogModel {
fn get_id(&self) -> String {
self.request_id
.map(|id| id.to_string())
.unwrap_or_else(|| self.task_id.clone())
}
}
impl Identifiable for Request {
fn get_id(&self) -> String {
self.id.to_string()
}
}
impl Identifiable for Response {
fn get_id(&self) -> String {
self.id.to_string()
}
}
impl Identifiable for TaskParserEvent {
fn get_id(&self) -> String {
self.id.to_string()
}
}
impl Identifiable for TaskErrorEvent {
fn get_id(&self) -> String {
self.id.to_string()
}
}
impl Identifiable for TaskEvent {
fn get_id(&self) -> String {
self.run_id.to_string()
}
fn partition_key(&self) -> String {
self.account.clone()
}
}
#[async_trait]
pub trait Compensator: Send + Sync {
async fn add_task(&self, topic: &str, id: &str, payload: Arc<Vec<u8>>) -> Result<()>;
async fn remove_task(&self, topic: &str, id: &str) -> Result<()>;
}
enum CompensationMessage {
Add {
topic: String,
id: String,
payload: Arc<Vec<u8>>,
},
Remove {
topic: String,
id: String,
},
}
pub struct RedisCompensator {
sender: mpsc::Sender<CompensationMessage>,
}
impl RedisCompensator {
pub fn new(redis_config: &RedisConfig, namespace: &str) -> Result<Self> {
let server = format!("{}:{}", redis_config.redis_host, redis_config.redis_port);
let db = redis_config.redis_db;
let redis_url = match (&redis_config.redis_username, &redis_config.redis_password) {
(Some(user), Some(pass)) => {
format!("redis://{user}:{pass}@{server}/{db}?protocol=resp3")
}
(Some(user), None) => format!("redis://{user}@{server}/{db}?protocol=resp3"),
(None, Some(pass)) => format!("redis://:{pass}@{server}/{db}?protocol=resp3"),
(None, None) => format!("redis://{server}/{db}?protocol=resp3"),
};
let client = redis::Client::open(redis_url).map_err(|_| QueueError::ConnectionFailed)?;
let (tx, mut rx) = mpsc::channel(10000);
let namespace = namespace.to_string();
tokio::spawn(async move {
let mut conn = loop {
match client.get_multiplexed_async_connection().await {
Ok(c) => break Some(c),
Err(e) => {
error!(
"Failed to connect to Redis in compensator: {}. Retrying in 1s...",
e
);
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
}
};
let mut batch: Vec<CompensationMessage> = Vec::with_capacity(100);
const MAX_BATCH_BACKLOG: usize = 10_000;
while let Some(msg) = rx.recv().await {
batch.push(msg);
while batch.len() < 100 {
match rx.try_recv() {
Ok(m) => batch.push(m),
Err(_) => break,
}
}
if batch.len() > MAX_BATCH_BACKLOG {
let dropped = batch.len() - MAX_BATCH_BACKLOG;
batch.drain(..dropped);
warn!(
"Compensator batch exceeded {} limit, dropped {} oldest messages",
MAX_BATCH_BACKLOG, dropped
);
}
if conn.is_none() {
let mut reconnect_attempts = 0u32;
loop {
match client.get_multiplexed_async_connection().await {
Ok(c) => {
conn = Some(c);
break;
}
Err(e) => {
reconnect_attempts += 1;
if reconnect_attempts >= 5 {
error!(
"Compensator failed to reconnect after {} attempts: {}. \
Keeping {} messages for next cycle.",
reconnect_attempts,
e,
batch.len()
);
break;
}
let backoff = Duration::from_millis(
500 * (1u64 << reconnect_attempts.min(4)),
);
error!(
"Failed to reconnect to Redis in compensator (attempt {}): {}. Retrying in {:?}...",
reconnect_attempts, e, backoff
);
tokio::time::sleep(backoff).await;
}
}
}
if conn.is_none() {
continue;
}
}
let active_conn = match conn.as_mut() {
Some(c) => c,
None => {
continue;
}
};
let mut pipe = redis::pipe();
for msg in batch.drain(..) {
match msg {
CompensationMessage::Add { topic, id, payload } => {
let tag = format!("{{{}:compensation:{}}}", namespace, topic);
let zset_key = format!("{}:zset", tag);
let hash_key = format!("{}:data", tag);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
pipe.zadd(&zset_key, &id, now)
.hset(&hash_key, &id, payload.as_slice());
}
CompensationMessage::Remove { topic, id } => {
let tag = format!("{{{}:compensation:{}}}", namespace, topic);
let zset_key = format!("{}:zset", tag);
let hash_key = format!("{}:data", tag);
pipe.zrem(&zset_key, &id).hdel(&hash_key, &id);
}
}
}
if let Err(e) = pipe.query_async::<()>(active_conn).await {
error!(
"Failed to execute batch compensation task in Redis: {}. Resetting connection.",
e
);
conn = None; }
}
});
Ok(Self { sender: tx })
}
}
#[async_trait]
impl Compensator for RedisCompensator {
async fn add_task(&self, topic: &str, id: &str, payload: Arc<Vec<u8>>) -> Result<()> {
self.sender
.send(CompensationMessage::Add {
topic: topic.to_string(),
id: id.to_string(),
payload,
})
.await
.map_err(|e| QueueError::PushFailed(Box::new(e)))?;
Ok(())
}
async fn remove_task(&self, topic: &str, id: &str) -> Result<()> {
self.sender
.send(CompensationMessage::Remove {
topic: topic.to_string(),
id: id.to_string(),
})
.await
.map_err(|e| QueueError::PushFailed(Box::new(e)))?;
Ok(())
}
}