Skip to main content

llm_optimizer_processor/kafka/
error.rs

1//! Kafka-specific error types and error handling.
2//!
3//! This module provides comprehensive error types for all Kafka operations,
4//! including connection errors, serialization/deserialization errors,
5//! consumption errors, and production errors.
6
7use std::fmt;
8use thiserror::Error;
9
10/// Result type alias for Kafka operations.
11pub type Result<T> = std::result::Result<T, KafkaError>;
12
13/// Comprehensive error type for Kafka operations.
14///
15/// This enum covers all possible error scenarios when working with Kafka,
16/// providing detailed context for debugging and error handling.
17///
18/// # Examples
19///
20/// ```rust
21/// use processor::kafka::error::{KafkaError, Result};
22///
23/// fn connect_to_kafka() -> Result<()> {
24///     // Simulate connection error
25///     Err(KafkaError::Connection {
26///         message: "Failed to connect to broker".to_string(),
27///         broker: "localhost:9092".to_string(),
28///     })
29/// }
30///
31/// match connect_to_kafka() {
32///     Ok(_) => println!("Connected successfully"),
33///     Err(KafkaError::Connection { message, broker }) => {
34///         eprintln!("Connection error to {}: {}", broker, message);
35///     }
36///     Err(e) => eprintln!("Other error: {}", e),
37/// }
38/// ```
39#[derive(Error, Debug)]
40pub enum KafkaError {
41    /// Error establishing connection to Kafka broker.
42    #[error("Failed to connect to Kafka broker {broker}: {message}")]
43    Connection {
44        /// Error message describing the connection failure.
45        message: String,
46        /// Broker address that failed to connect.
47        broker: String,
48    },
49
50    /// Error during message consumption.
51    #[error("Failed to consume message from topic {topic}: {message}")]
52    Consumption {
53        /// Error message describing the consumption failure.
54        message: String,
55        /// Topic from which consumption failed.
56        topic: String,
57        /// Partition number if available.
58        partition: Option<i32>,
59        /// Offset if available.
60        offset: Option<i64>,
61    },
62
63    /// Error during message production.
64    #[error("Failed to produce message to topic {topic}: {message}")]
65    Production {
66        /// Error message describing the production failure.
67        message: String,
68        /// Topic to which production failed.
69        topic: String,
70        /// Partition number if available.
71        partition: Option<i32>,
72    },
73
74    /// Error during serialization of messages.
75    #[error("Serialization error: {message}")]
76    Serialization {
77        /// Error message describing the serialization failure.
78        message: String,
79        /// Optional context about what was being serialized.
80        context: Option<String>,
81    },
82
83    /// Error during deserialization of messages.
84    #[error("Deserialization error: {message}")]
85    Deserialization {
86        /// Error message describing the deserialization failure.
87        message: String,
88        /// Optional context about what was being deserialized.
89        context: Option<String>,
90        /// Raw bytes that failed to deserialize (truncated for display).
91        payload_sample: Option<Vec<u8>>,
92    },
93
94    /// Configuration validation error.
95    #[error("Invalid configuration: {message}")]
96    InvalidConfiguration {
97        /// Error message describing the configuration issue.
98        message: String,
99        /// Field name that has invalid configuration.
100        field: Option<String>,
101    },
102
103    /// Error committing offsets.
104    #[error("Failed to commit offsets for topic {topic}: {message}")]
105    OffsetCommit {
106        /// Error message describing the commit failure.
107        message: String,
108        /// Topic for which offset commit failed.
109        topic: String,
110        /// Partition for which offset commit failed.
111        partition: i32,
112        /// Offset that failed to commit.
113        offset: i64,
114    },
115
116    /// Error during consumer group coordination.
117    #[error("Consumer group coordination error for group {group_id}: {message}")]
118    GroupCoordination {
119        /// Error message describing the coordination failure.
120        message: String,
121        /// Consumer group ID.
122        group_id: String,
123    },
124
125    /// Error during partition assignment.
126    #[error("Partition assignment error: {message}")]
127    PartitionAssignment {
128        /// Error message describing the assignment failure.
129        message: String,
130        /// Topics involved in the assignment.
131        topics: Vec<String>,
132    },
133
134    /// Authentication or authorization error.
135    #[error("Authentication/Authorization error: {message}")]
136    AuthError {
137        /// Error message describing the auth failure.
138        message: String,
139        /// Security protocol being used.
140        protocol: String,
141    },
142
143    /// Network or I/O error.
144    #[error("Network I/O error: {message}")]
145    IoError {
146        /// Error message describing the I/O failure.
147        message: String,
148        /// Optional underlying I/O error.
149        #[source]
150        source: Option<std::io::Error>,
151    },
152
153    /// Timeout error during Kafka operations.
154    #[error("Operation timed out after {timeout_ms}ms: {operation}")]
155    Timeout {
156        /// Description of the operation that timed out.
157        operation: String,
158        /// Timeout duration in milliseconds.
159        timeout_ms: u64,
160    },
161
162    /// Error related to topic metadata.
163    #[error("Topic metadata error for topic {topic}: {message}")]
164    TopicMetadata {
165        /// Error message describing the metadata failure.
166        message: String,
167        /// Topic name.
168        topic: String,
169    },
170
171    /// Error when a topic partition doesn't exist.
172    #[error("Topic partition {topic}:{partition} does not exist")]
173    PartitionNotFound {
174        /// Topic name.
175        topic: String,
176        /// Partition number.
177        partition: i32,
178    },
179
180    /// Error when reaching resource limits.
181    #[error("Resource limit exceeded: {message}")]
182    ResourceExhausted {
183        /// Error message describing the resource exhaustion.
184        message: String,
185        /// Resource type (e.g., "buffer", "connections").
186        resource: String,
187    },
188
189    /// Error from the underlying rdkafka library.
190    #[error("Kafka client error: {message} (error code: {code:?})")]
191    KafkaClient {
192        /// Error message from rdkafka.
193        message: String,
194        /// rdkafka error code.
195        code: Option<i32>,
196    },
197
198    /// Generic error for cases not covered by specific variants.
199    #[error("Kafka error: {0}")]
200    Other(String),
201}
202
203impl KafkaError {
204    /// Create a connection error.
205    pub fn connection(message: impl Into<String>, broker: impl Into<String>) -> Self {
206        Self::Connection {
207            message: message.into(),
208            broker: broker.into(),
209        }
210    }
211
212    /// Create a consumption error.
213    pub fn consumption(
214        message: impl Into<String>,
215        topic: impl Into<String>,
216        partition: Option<i32>,
217        offset: Option<i64>,
218    ) -> Self {
219        Self::Consumption {
220            message: message.into(),
221            topic: topic.into(),
222            partition,
223            offset,
224        }
225    }
226
227    /// Create a production error.
228    pub fn production(
229        message: impl Into<String>,
230        topic: impl Into<String>,
231        partition: Option<i32>,
232    ) -> Self {
233        Self::Production {
234            message: message.into(),
235            topic: topic.into(),
236            partition,
237        }
238    }
239
240    /// Create a serialization error.
241    pub fn serialization(message: impl Into<String>, context: Option<String>) -> Self {
242        Self::Serialization {
243            message: message.into(),
244            context,
245        }
246    }
247
248    /// Create a deserialization error.
249    pub fn deserialization(
250        message: impl Into<String>,
251        context: Option<String>,
252        payload_sample: Option<Vec<u8>>,
253    ) -> Self {
254        Self::Deserialization {
255            message: message.into(),
256            context,
257            payload_sample,
258        }
259    }
260
261    /// Create an invalid configuration error.
262    pub fn invalid_config(message: impl Into<String>, field: Option<String>) -> Self {
263        Self::InvalidConfiguration {
264            message: message.into(),
265            field,
266        }
267    }
268
269    /// Create an offset commit error.
270    pub fn offset_commit(
271        message: impl Into<String>,
272        topic: impl Into<String>,
273        partition: i32,
274        offset: i64,
275    ) -> Self {
276        Self::OffsetCommit {
277            message: message.into(),
278            topic: topic.into(),
279            partition,
280            offset,
281        }
282    }
283
284    /// Create a group coordination error.
285    pub fn group_coordination(message: impl Into<String>, group_id: impl Into<String>) -> Self {
286        Self::GroupCoordination {
287            message: message.into(),
288            group_id: group_id.into(),
289        }
290    }
291
292    /// Create a partition assignment error.
293    pub fn partition_assignment(message: impl Into<String>, topics: Vec<String>) -> Self {
294        Self::PartitionAssignment {
295            message: message.into(),
296            topics,
297        }
298    }
299
300    /// Create an authentication/authorization error.
301    pub fn auth_error(message: impl Into<String>, protocol: impl Into<String>) -> Self {
302        Self::AuthError {
303            message: message.into(),
304            protocol: protocol.into(),
305        }
306    }
307
308    /// Create a timeout error.
309    pub fn timeout(operation: impl Into<String>, timeout_ms: u64) -> Self {
310        Self::Timeout {
311            operation: operation.into(),
312            timeout_ms,
313        }
314    }
315
316    /// Create a topic metadata error.
317    pub fn topic_metadata(message: impl Into<String>, topic: impl Into<String>) -> Self {
318        Self::TopicMetadata {
319            message: message.into(),
320            topic: topic.into(),
321        }
322    }
323
324    /// Create a partition not found error.
325    pub fn partition_not_found(topic: impl Into<String>, partition: i32) -> Self {
326        Self::PartitionNotFound {
327            topic: topic.into(),
328            partition,
329        }
330    }
331
332    /// Create a resource exhausted error.
333    pub fn resource_exhausted(message: impl Into<String>, resource: impl Into<String>) -> Self {
334        Self::ResourceExhausted {
335            message: message.into(),
336            resource: resource.into(),
337        }
338    }
339
340    /// Create a Kafka client error.
341    pub fn kafka_client(message: impl Into<String>, code: Option<i32>) -> Self {
342        Self::KafkaClient {
343            message: message.into(),
344            code,
345        }
346    }
347
348    /// Check if the error is retryable.
349    ///
350    /// Returns `true` if the operation that caused this error can be retried.
351    pub fn is_retryable(&self) -> bool {
352        matches!(
353            self,
354            KafkaError::Timeout { .. }
355                | KafkaError::IoError { .. }
356                | KafkaError::Connection { .. }
357                | KafkaError::GroupCoordination { .. }
358                | KafkaError::ResourceExhausted { .. }
359        )
360    }
361
362    /// Check if the error is fatal and requires system shutdown.
363    ///
364    /// Returns `true` if the error indicates an unrecoverable state.
365    pub fn is_fatal(&self) -> bool {
366        matches!(
367            self,
368            KafkaError::AuthError { .. } | KafkaError::InvalidConfiguration { .. }
369        )
370    }
371
372    /// Get error severity level.
373    ///
374    /// Returns a severity level suitable for logging and monitoring.
375    pub fn severity(&self) -> ErrorSeverity {
376        match self {
377            KafkaError::InvalidConfiguration { .. } | KafkaError::AuthError { .. } => {
378                ErrorSeverity::Critical
379            }
380            KafkaError::Connection { .. }
381            | KafkaError::GroupCoordination { .. }
382            | KafkaError::Production { .. } => ErrorSeverity::Error,
383            KafkaError::Timeout { .. }
384            | KafkaError::ResourceExhausted { .. }
385            | KafkaError::Consumption { .. } => ErrorSeverity::Warning,
386            _ => ErrorSeverity::Info,
387        }
388    }
389}
390
391/// Error severity levels.
392#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
393pub enum ErrorSeverity {
394    /// Informational messages.
395    Info,
396    /// Warning messages for potential issues.
397    Warning,
398    /// Error messages for failures.
399    Error,
400    /// Critical errors requiring immediate attention.
401    Critical,
402}
403
404impl fmt::Display for ErrorSeverity {
405    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
406        match self {
407            ErrorSeverity::Info => write!(f, "INFO"),
408            ErrorSeverity::Warning => write!(f, "WARNING"),
409            ErrorSeverity::Error => write!(f, "ERROR"),
410            ErrorSeverity::Critical => write!(f, "CRITICAL"),
411        }
412    }
413}
414
415// Conversions from common error types
416
417impl From<std::io::Error> for KafkaError {
418    fn from(err: std::io::Error) -> Self {
419        KafkaError::IoError {
420            message: err.to_string(),
421            source: Some(err),
422        }
423    }
424}
425
426impl From<serde_json::Error> for KafkaError {
427    fn from(err: serde_json::Error) -> Self {
428        KafkaError::Serialization {
429            message: err.to_string(),
430            context: Some("JSON serialization".to_string()),
431        }
432    }
433}
434
435impl From<bincode::Error> for KafkaError {
436    fn from(err: bincode::Error) -> Self {
437        KafkaError::Serialization {
438            message: err.to_string(),
439            context: Some("Bincode serialization".to_string()),
440        }
441    }
442}
443
444// Conversion from rdkafka errors
445impl From<rdkafka::error::KafkaError> for KafkaError {
446    fn from(err: rdkafka::error::KafkaError) -> Self {
447        use rdkafka::error::KafkaError as RdKafkaError;
448
449        match err {
450            // Connection-related errors
451            RdKafkaError::ClientCreation(msg) => KafkaError::connection(msg, "unknown"),
452
453            // Metadata errors
454            RdKafkaError::MetadataFetch(err) => {
455                KafkaError::topic_metadata(err.to_string(), "unknown")
456            }
457
458            // Subscription errors
459            RdKafkaError::Subscription(msg) => {
460                KafkaError::partition_assignment(msg, Vec::new())
461            }
462
463            // Group coordination errors
464            RdKafkaError::GroupListFetch(err) => {
465                KafkaError::group_coordination(err.to_string(), "unknown")
466            }
467
468            // Message production errors
469            RdKafkaError::MessageProduction(code) => KafkaError::production(
470                format!("Message production failed with code: {:?}", code),
471                "unknown",
472                None,
473            ),
474
475            // Message consumption errors
476            RdKafkaError::MessageConsumption(code) => KafkaError::consumption(
477                format!("Message consumption failed with code: {:?}", code),
478                "unknown",
479                None,
480                None,
481            ),
482
483            // Offset store/commit errors
484            RdKafkaError::StoreOffset(err) | RdKafkaError::OffsetFetch(err) => {
485                KafkaError::offset_commit(err.to_string(), "unknown", -1, -1)
486            }
487
488            // Partition EOF (not really an error, but can be handled)
489            RdKafkaError::PartitionEOF(partition) => KafkaError::consumption(
490                format!("Reached end of partition {}", partition),
491                "unknown",
492                Some(partition),
493                None,
494            ),
495
496            // Generic error
497            _ => KafkaError::kafka_client(err.to_string(), None),
498        }
499    }
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505
506    #[test]
507    fn test_error_creation() {
508        let err = KafkaError::connection("Connection refused", "localhost:9092");
509        assert!(matches!(err, KafkaError::Connection { .. }));
510    }
511
512    #[test]
513    fn test_error_is_retryable() {
514        let timeout_err = KafkaError::timeout("poll", 1000);
515        assert!(timeout_err.is_retryable());
516
517        let auth_err = KafkaError::auth_error("Invalid credentials", "SASL_SSL");
518        assert!(!auth_err.is_retryable());
519    }
520
521    #[test]
522    fn test_error_is_fatal() {
523        let auth_err = KafkaError::auth_error("Invalid credentials", "SASL_SSL");
524        assert!(auth_err.is_fatal());
525
526        let timeout_err = KafkaError::timeout("poll", 1000);
527        assert!(!timeout_err.is_fatal());
528    }
529
530    #[test]
531    fn test_error_severity() {
532        let critical = KafkaError::invalid_config("Missing bootstrap servers", None);
533        assert_eq!(critical.severity(), ErrorSeverity::Critical);
534
535        let error = KafkaError::connection("Connection refused", "localhost:9092");
536        assert_eq!(error.severity(), ErrorSeverity::Error);
537
538        let warning = KafkaError::timeout("poll", 1000);
539        assert_eq!(warning.severity(), ErrorSeverity::Warning);
540    }
541
542    #[test]
543    fn test_error_display() {
544        let err = KafkaError::connection("Connection refused", "localhost:9092");
545        let display = format!("{}", err);
546        assert!(display.contains("localhost:9092"));
547        assert!(display.contains("Connection refused"));
548    }
549
550    #[test]
551    fn test_io_error_conversion() {
552        let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "test error");
553        let kafka_err: KafkaError = io_err.into();
554        assert!(matches!(kafka_err, KafkaError::IoError { .. }));
555    }
556
557    #[test]
558    fn test_serde_error_conversion() {
559        let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
560        let kafka_err: KafkaError = json_err.into();
561        assert!(matches!(kafka_err, KafkaError::Serialization { .. }));
562    }
563}