use crate::error::MqError;
use crate::queue::{Message, MessageQueue};
use async_trait::async_trait;
use rdkafka::config::{ClientConfig, RDKafkaLogLevel};
use rdkafka::consumer::{CommitMode, StreamConsumer};
use rdkafka::message::Message as _;
use rdkafka::producer::{FutureProducer, FutureRecord};
use rdkafka::TopicPartitionList;
use std::collections::HashMap;
use std::sync::Arc;
pub struct RealKafkaQueue {
brokers: String,
group_id: String,
producer: Option<Arc<FutureProducer>>,
consumer: Option<Arc<StreamConsumer>>,
}
impl RealKafkaQueue {
pub fn new(brokers: impl Into<String>, group_id: impl Into<String>) -> Self {
Self {
brokers: brokers.into(),
group_id: group_id.into(),
producer: None,
consumer: None,
}
}
pub async fn connect_producer(&mut self) -> Result<(), MqError> {
let producer: FutureProducer = ClientConfig::new()
.set("bootstrap.servers", &self.brokers)
.set("message.timeout.ms", "5000")
.set_log_level(RDKafkaLogLevel::Warning)
.create()
.map_err(|e| MqError::Connection(format!("Kafka producer failed: {e}")))?;
self.producer = Some(Arc::new(producer));
Ok(())
}
pub async fn connect_consumer(&mut self) -> Result<(), MqError> {
let consumer: StreamConsumer = ClientConfig::new()
.set("bootstrap.servers", &self.brokers)
.set("group.id", &self.group_id)
.set("enable.auto.commit", "false")
.set("session.timeout.ms", "6000")
.set_log_level(RDKafkaLogLevel::Warning)
.create()
.map_err(|e| MqError::Connection(format!("Kafka consumer failed: {e}")))?;
self.consumer = Some(Arc::new(consumer));
Ok(())
}
pub async fn reconnect(&mut self) -> Result<(), MqError> {
self.producer = None;
self.consumer = None;
self.connect_producer().await?;
self.connect_consumer().await?;
Ok(())
}
}
impl Default for RealKafkaQueue {
fn default() -> Self {
Self::new("localhost:9092", "sz-orm-group")
}
}
#[async_trait]
impl MessageQueue for RealKafkaQueue {
async fn publish(&self, topic: &str, message: &[u8]) -> Result<(), MqError> {
let producer = self
.producer
.as_ref()
.ok_or_else(|| MqError::Connection("Kafka producer not connected".into()))?;
let delivery = producer
.send(
FutureRecord::to(topic).payload(message),
std::time::Duration::from_secs(5),
)
.await;
match delivery {
Ok(_) => Ok(()),
Err((e, _)) => Err(MqError::Publish(format!("Kafka send failed: {e}"))),
}
}
async fn consume(&self, topic: &str) -> Result<Option<Message>, MqError> {
let consumer = self
.consumer
.as_ref()
.ok_or_else(|| MqError::Connection("Kafka consumer not connected".into()))?;
consumer
.subscribe(&[topic])
.map_err(|e| MqError::Subscribe(format!("Kafka subscribe failed: {e}")))?;
match tokio::time::timeout(std::time::Duration::from_millis(100), consumer.recv()).await {
Ok(Ok(msg)) => {
let payload = msg.payload().map(|p| p.to_vec()).unwrap_or_default();
let key = msg.key().map(|k| String::from_utf8_lossy(k).to_string());
let partition = msg.partition();
let offset = msg.offset();
let msg_id = format!("{topic}-{partition}-{offset}");
let message = Message {
topic: topic.to_string(),
payload,
key,
timestamp: current_timestamp_millis(),
headers: HashMap::new(),
id: msg_id,
};
Ok(Some(message))
}
Ok(Err(e)) => Err(MqError::Publish(format!("Kafka recv failed: {e}"))),
Err(_) => Ok(None), }
}
async fn ack(&self, message_id: &str) -> Result<(), MqError> {
let consumer = self
.consumer
.as_ref()
.ok_or_else(|| MqError::Connection("Kafka consumer not connected".into()))?;
let parts: Vec<&str> = message_id.rsplitn(3, '-').collect();
if parts.len() != 3 {
return Err(MqError::NotSupported(format!(
"invalid Kafka message_id (expected topic-partition-offset): {}",
message_id
)));
}
let offset: i64 = parts[0]
.parse()
.map_err(|_| MqError::NotSupported(format!("invalid offset: {}", parts[0])))?;
let partition: i32 = parts[1]
.parse()
.map_err(|_| MqError::NotSupported(format!("invalid partition: {}", parts[1])))?;
let topic = parts[2];
let mut tpl = TopicPartitionList::new();
tpl.add_partition_offset(topic, partition, rdkafka::Offset::Offset(offset + 1))
.map_err(|e| MqError::Publish(format!("Kafka add partition failed: {e}")))?;
consumer
.commit(&tpl, CommitMode::Async)
.map_err(|e| MqError::Publish(format!("Kafka commit failed: {e}")))?;
Ok(())
}
async fn subscribe(&self, topic: &str) -> Result<(), MqError> {
let consumer = self
.consumer
.as_ref()
.ok_or_else(|| MqError::Connection("Kafka consumer not connected".into()))?;
consumer
.subscribe(&[topic])
.map_err(|e| MqError::Subscribe(format!("Kafka subscribe failed: {e}")))?;
Ok(())
}
}
fn current_timestamp_millis() -> i64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_real_kafka_queue_new() {
let queue = RealKafkaQueue::new("localhost:9092", "test-group");
assert_eq!(queue.brokers, "localhost:9092");
assert_eq!(queue.group_id, "test-group");
assert!(queue.producer.is_none());
assert!(queue.consumer.is_none());
}
#[test]
fn test_real_kafka_queue_default() {
let queue = RealKafkaQueue::default();
assert_eq!(queue.brokers, "localhost:9092");
assert_eq!(queue.group_id, "sz-orm-group");
}
#[tokio::test]
async fn test_real_kafka_not_connected_publish() {
let queue = RealKafkaQueue::new("localhost:9092", "test-group");
let result = queue.publish("topic", b"msg").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_real_kafka_not_connected_consume() {
let queue = RealKafkaQueue::new("localhost:9092", "test-group");
let result = queue.consume("topic").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_real_kafka_not_connected_subscribe() {
let queue = RealKafkaQueue::new("localhost:9092", "test-group");
let result = queue.subscribe("topic").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_real_kafka_ack_not_connected_fails() {
let queue = RealKafkaQueue::new("localhost:9092", "test-group");
let result = queue.ack("test-topic-0-100").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_real_kafka_ack_invalid_message_id_format() {
let queue = RealKafkaQueue::new("localhost:9092", "test-group");
let result = queue.ack("invalid-id-no-dashes").await;
assert!(result.is_err());
}
#[tokio::test]
#[ignore = "需真实 Kafka 服务器"]
async fn test_real_kafka_publish_and_consume() {
let mut queue = RealKafkaQueue::new("localhost:9092", "test-group");
queue.connect_producer().await.unwrap();
queue.connect_consumer().await.unwrap();
queue.subscribe("test-topic").await.unwrap();
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
queue.publish("test-topic", b"hello kafka").await.unwrap();
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let msg = queue
.consume("test-topic")
.await
.unwrap()
.expect("message should exist");
assert_eq!(msg.payload, b"hello kafka");
assert_eq!(msg.topic, "test-topic");
queue.ack(&msg.id).await.unwrap();
}
}