use std::fmt;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, KafkaError>;
#[derive(Error, Debug)]
pub enum KafkaError {
#[error("Failed to connect to Kafka broker {broker}: {message}")]
Connection {
message: String,
broker: String,
},
#[error("Failed to consume message from topic {topic}: {message}")]
Consumption {
message: String,
topic: String,
partition: Option<i32>,
offset: Option<i64>,
},
#[error("Failed to produce message to topic {topic}: {message}")]
Production {
message: String,
topic: String,
partition: Option<i32>,
},
#[error("Serialization error: {message}")]
Serialization {
message: String,
context: Option<String>,
},
#[error("Deserialization error: {message}")]
Deserialization {
message: String,
context: Option<String>,
payload_sample: Option<Vec<u8>>,
},
#[error("Invalid configuration: {message}")]
InvalidConfiguration {
message: String,
field: Option<String>,
},
#[error("Failed to commit offsets for topic {topic}: {message}")]
OffsetCommit {
message: String,
topic: String,
partition: i32,
offset: i64,
},
#[error("Consumer group coordination error for group {group_id}: {message}")]
GroupCoordination {
message: String,
group_id: String,
},
#[error("Partition assignment error: {message}")]
PartitionAssignment {
message: String,
topics: Vec<String>,
},
#[error("Authentication/Authorization error: {message}")]
AuthError {
message: String,
protocol: String,
},
#[error("Network I/O error: {message}")]
IoError {
message: String,
#[source]
source: Option<std::io::Error>,
},
#[error("Operation timed out after {timeout_ms}ms: {operation}")]
Timeout {
operation: String,
timeout_ms: u64,
},
#[error("Topic metadata error for topic {topic}: {message}")]
TopicMetadata {
message: String,
topic: String,
},
#[error("Topic partition {topic}:{partition} does not exist")]
PartitionNotFound {
topic: String,
partition: i32,
},
#[error("Resource limit exceeded: {message}")]
ResourceExhausted {
message: String,
resource: String,
},
#[error("Kafka client error: {message} (error code: {code:?})")]
KafkaClient {
message: String,
code: Option<i32>,
},
#[error("Kafka error: {0}")]
Other(String),
}
impl KafkaError {
pub fn connection(message: impl Into<String>, broker: impl Into<String>) -> Self {
Self::Connection {
message: message.into(),
broker: broker.into(),
}
}
pub fn consumption(
message: impl Into<String>,
topic: impl Into<String>,
partition: Option<i32>,
offset: Option<i64>,
) -> Self {
Self::Consumption {
message: message.into(),
topic: topic.into(),
partition,
offset,
}
}
pub fn production(
message: impl Into<String>,
topic: impl Into<String>,
partition: Option<i32>,
) -> Self {
Self::Production {
message: message.into(),
topic: topic.into(),
partition,
}
}
pub fn serialization(message: impl Into<String>, context: Option<String>) -> Self {
Self::Serialization {
message: message.into(),
context,
}
}
pub fn deserialization(
message: impl Into<String>,
context: Option<String>,
payload_sample: Option<Vec<u8>>,
) -> Self {
Self::Deserialization {
message: message.into(),
context,
payload_sample,
}
}
pub fn invalid_config(message: impl Into<String>, field: Option<String>) -> Self {
Self::InvalidConfiguration {
message: message.into(),
field,
}
}
pub fn offset_commit(
message: impl Into<String>,
topic: impl Into<String>,
partition: i32,
offset: i64,
) -> Self {
Self::OffsetCommit {
message: message.into(),
topic: topic.into(),
partition,
offset,
}
}
pub fn group_coordination(message: impl Into<String>, group_id: impl Into<String>) -> Self {
Self::GroupCoordination {
message: message.into(),
group_id: group_id.into(),
}
}
pub fn partition_assignment(message: impl Into<String>, topics: Vec<String>) -> Self {
Self::PartitionAssignment {
message: message.into(),
topics,
}
}
pub fn auth_error(message: impl Into<String>, protocol: impl Into<String>) -> Self {
Self::AuthError {
message: message.into(),
protocol: protocol.into(),
}
}
pub fn timeout(operation: impl Into<String>, timeout_ms: u64) -> Self {
Self::Timeout {
operation: operation.into(),
timeout_ms,
}
}
pub fn topic_metadata(message: impl Into<String>, topic: impl Into<String>) -> Self {
Self::TopicMetadata {
message: message.into(),
topic: topic.into(),
}
}
pub fn partition_not_found(topic: impl Into<String>, partition: i32) -> Self {
Self::PartitionNotFound {
topic: topic.into(),
partition,
}
}
pub fn resource_exhausted(message: impl Into<String>, resource: impl Into<String>) -> Self {
Self::ResourceExhausted {
message: message.into(),
resource: resource.into(),
}
}
pub fn kafka_client(message: impl Into<String>, code: Option<i32>) -> Self {
Self::KafkaClient {
message: message.into(),
code,
}
}
pub fn is_retryable(&self) -> bool {
matches!(
self,
KafkaError::Timeout { .. }
| KafkaError::IoError { .. }
| KafkaError::Connection { .. }
| KafkaError::GroupCoordination { .. }
| KafkaError::ResourceExhausted { .. }
)
}
pub fn is_fatal(&self) -> bool {
matches!(
self,
KafkaError::AuthError { .. } | KafkaError::InvalidConfiguration { .. }
)
}
pub fn severity(&self) -> ErrorSeverity {
match self {
KafkaError::InvalidConfiguration { .. } | KafkaError::AuthError { .. } => {
ErrorSeverity::Critical
}
KafkaError::Connection { .. }
| KafkaError::GroupCoordination { .. }
| KafkaError::Production { .. } => ErrorSeverity::Error,
KafkaError::Timeout { .. }
| KafkaError::ResourceExhausted { .. }
| KafkaError::Consumption { .. } => ErrorSeverity::Warning,
_ => ErrorSeverity::Info,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ErrorSeverity {
Info,
Warning,
Error,
Critical,
}
impl fmt::Display for ErrorSeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErrorSeverity::Info => write!(f, "INFO"),
ErrorSeverity::Warning => write!(f, "WARNING"),
ErrorSeverity::Error => write!(f, "ERROR"),
ErrorSeverity::Critical => write!(f, "CRITICAL"),
}
}
}
impl From<std::io::Error> for KafkaError {
fn from(err: std::io::Error) -> Self {
KafkaError::IoError {
message: err.to_string(),
source: Some(err),
}
}
}
impl From<serde_json::Error> for KafkaError {
fn from(err: serde_json::Error) -> Self {
KafkaError::Serialization {
message: err.to_string(),
context: Some("JSON serialization".to_string()),
}
}
}
impl From<bincode::Error> for KafkaError {
fn from(err: bincode::Error) -> Self {
KafkaError::Serialization {
message: err.to_string(),
context: Some("Bincode serialization".to_string()),
}
}
}
impl From<rdkafka::error::KafkaError> for KafkaError {
fn from(err: rdkafka::error::KafkaError) -> Self {
use rdkafka::error::KafkaError as RdKafkaError;
match err {
RdKafkaError::ClientCreation(msg) => KafkaError::connection(msg, "unknown"),
RdKafkaError::MetadataFetch(err) => {
KafkaError::topic_metadata(err.to_string(), "unknown")
}
RdKafkaError::Subscription(msg) => {
KafkaError::partition_assignment(msg, Vec::new())
}
RdKafkaError::GroupListFetch(err) => {
KafkaError::group_coordination(err.to_string(), "unknown")
}
RdKafkaError::MessageProduction(code) => KafkaError::production(
format!("Message production failed with code: {:?}", code),
"unknown",
None,
),
RdKafkaError::MessageConsumption(code) => KafkaError::consumption(
format!("Message consumption failed with code: {:?}", code),
"unknown",
None,
None,
),
RdKafkaError::StoreOffset(err) | RdKafkaError::OffsetFetch(err) => {
KafkaError::offset_commit(err.to_string(), "unknown", -1, -1)
}
RdKafkaError::PartitionEOF(partition) => KafkaError::consumption(
format!("Reached end of partition {}", partition),
"unknown",
Some(partition),
None,
),
_ => KafkaError::kafka_client(err.to_string(), None),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_creation() {
let err = KafkaError::connection("Connection refused", "localhost:9092");
assert!(matches!(err, KafkaError::Connection { .. }));
}
#[test]
fn test_error_is_retryable() {
let timeout_err = KafkaError::timeout("poll", 1000);
assert!(timeout_err.is_retryable());
let auth_err = KafkaError::auth_error("Invalid credentials", "SASL_SSL");
assert!(!auth_err.is_retryable());
}
#[test]
fn test_error_is_fatal() {
let auth_err = KafkaError::auth_error("Invalid credentials", "SASL_SSL");
assert!(auth_err.is_fatal());
let timeout_err = KafkaError::timeout("poll", 1000);
assert!(!timeout_err.is_fatal());
}
#[test]
fn test_error_severity() {
let critical = KafkaError::invalid_config("Missing bootstrap servers", None);
assert_eq!(critical.severity(), ErrorSeverity::Critical);
let error = KafkaError::connection("Connection refused", "localhost:9092");
assert_eq!(error.severity(), ErrorSeverity::Error);
let warning = KafkaError::timeout("poll", 1000);
assert_eq!(warning.severity(), ErrorSeverity::Warning);
}
#[test]
fn test_error_display() {
let err = KafkaError::connection("Connection refused", "localhost:9092");
let display = format!("{}", err);
assert!(display.contains("localhost:9092"));
assert!(display.contains("Connection refused"));
}
#[test]
fn test_io_error_conversion() {
let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "test error");
let kafka_err: KafkaError = io_err.into();
assert!(matches!(kafka_err, KafkaError::IoError { .. }));
}
#[test]
fn test_serde_error_conversion() {
let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
let kafka_err: KafkaError = json_err.into();
assert!(matches!(kafka_err, KafkaError::Serialization { .. }));
}
}