Skip to main content

rskit_messaging/
managed_consumer.rs

1//! Managed consumer that wraps any [`MessageConsumer`] with lifecycle
2//! management, handler dispatch, and graceful shutdown.
3
4use std::sync::Arc;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::time::Instant;
7
8use rskit_errors::{AppError, AppResult, ErrorCode};
9use rskit_resilience::{LinearBackoff, RetryPolicy};
10use rskit_stream::SpawnedTask;
11use tracing;
12
13use crate::handler::MessageHandler;
14use crate::metrics::{MetricsCollector, NoopMetrics};
15use crate::traits::MessageConsumer;
16
17/// Wraps a [`MessageConsumer`] with lifecycle, handler dispatch, and
18/// graceful shutdown.
19///
20/// Use [`ManagedConsumerBuilder`] to construct an instance.
21pub struct ManagedConsumer<T: Send + Sync + Clone + 'static> {
22    inner: Arc<dyn MessageConsumer<T>>,
23    handler: Arc<dyn MessageHandler<T>>,
24    metrics: Arc<dyn MetricsCollector>,
25    recv_backoff: RetryPolicy,
26    name: String,
27    running: Arc<AtomicBool>,
28    task: parking_lot::Mutex<Option<SpawnedTask>>,
29}
30
31impl<T: Send + Sync + Clone + 'static> ManagedConsumer<T> {
32    /// Returns the name of this managed consumer.
33    pub fn name(&self) -> &str {
34        &self.name
35    }
36
37    /// Returns `true` when the consumer loop is running.
38    pub fn is_running(&self) -> bool {
39        self.running.load(Ordering::SeqCst)
40    }
41
42    /// Start the consumption loop in a background tokio task.
43    pub fn start(&self) -> AppResult<()> {
44        if self.running.load(Ordering::SeqCst) {
45            return Err(AppError::new(
46                ErrorCode::InvalidInput,
47                format!("consumer '{}' is already running", self.name),
48            ));
49        }
50
51        let consumer = self.inner.clone();
52        let handler = self.handler.clone();
53        let metrics = self.metrics.clone();
54        let recv_backoff = self.recv_backoff.clone();
55        let running = self.running.clone();
56        let name = self.name.clone();
57
58        running.store(true, Ordering::SeqCst);
59
60        let task = SpawnedTask::spawn(move |cancel| async move {
61            tracing::debug!(consumer = %name, "managed consumer loop started");
62            let mut consecutive_errors: u32 = 0;
63            loop {
64                tokio::select! {
65                    () = cancel.cancelled() => {
66                        tracing::info!(consumer = %name, "managed consumer cancelled");
67                        break;
68                    }
69                    result = consumer.recv() => {
70                        match result {
71                            Ok(msg) => {
72                                consecutive_errors = 0;
73                                let topic = msg.topic.clone();
74                                let start = Instant::now();
75                                let handle_result = handler.handle(msg).await;
76                                metrics.record_consume(
77                                    &topic,
78                                    start.elapsed(),
79                                    handle_result.is_ok(),
80                                );
81                                if let Err(e) = handle_result {
82                                    tracing::warn!(
83                                        consumer = %name,
84                                        error = %e,
85                                        "handler error"
86                                    );
87                                }
88                            }
89                            Err(e) => {
90                                if cancel.is_cancelled() {
91                                    break;
92                                }
93                                consecutive_errors += 1;
94
95                                let err_msg = e.to_string();
96                                let is_topic_missing = err_msg.contains("UnknownTopicOrPartition")
97                                    || err_msg.contains("UNKNOWN_TOPIC_OR_PARTITION")
98                                    || err_msg.contains("unknown topic");
99
100                                if is_topic_missing {
101                                    // Topic-not-found is expected during startup; log sparingly at debug.
102                                    if consecutive_errors == 1 {
103                                        tracing::debug!(
104                                            consumer = %name,
105                                            consecutive = consecutive_errors,
106                                            "topic not yet available, waiting for creation"
107                                        );
108                                    } else if consecutive_errors.is_multiple_of(30) {
109                                        tracing::info!(
110                                            consumer = %name,
111                                            consecutive = consecutive_errors,
112                                            "topic still not available"
113                                        );
114                                    }
115                                } else if consecutive_errors == 1 || consecutive_errors.is_multiple_of(10) {
116                                    tracing::warn!(
117                                        consumer = %name,
118                                        error = %e,
119                                        consecutive = consecutive_errors,
120                                        "recv error (retrying)"
121                                    );
122                                }
123                                tokio::time::sleep(
124                                    recv_backoff.backoff_delay(consecutive_errors as usize),
125                                )
126                                .await;
127                            }
128                        }
129                    }
130                }
131            }
132            running.store(false, Ordering::SeqCst);
133            tracing::debug!(consumer = %name, "managed consumer loop exited");
134        });
135
136        *self.task.lock() = Some(task);
137
138        Ok(())
139    }
140
141    /// Stop the consumption loop and wait for it to drain.
142    pub async fn stop(&self) -> AppResult<()> {
143        if !self.running.load(Ordering::SeqCst) {
144            return Err(AppError::new(
145                ErrorCode::InvalidInput,
146                format!("consumer '{}' is not running", self.name),
147            ));
148        }
149
150        let task = {
151            let mut guard = self.task.lock();
152            guard.take()
153        };
154
155        if let Some(task) = task {
156            task.shutdown(std::time::Duration::from_secs(10)).await;
157        }
158
159        // Call close() to release broker connections and resources.
160        if let Err(e) = self.inner.close().await {
161            tracing::warn!(consumer = %self.name, error = %e, "error closing consumer");
162        }
163
164        self.running.store(false, Ordering::SeqCst);
165        tracing::debug!(consumer = %self.name, "managed consumer stopped");
166        Ok(())
167    }
168}
169
170/// Builder for [`ManagedConsumer`].
171pub struct ManagedConsumerBuilder<T: Send + Sync + Clone + 'static> {
172    inner: Arc<dyn MessageConsumer<T>>,
173    handler: Arc<dyn MessageHandler<T>>,
174    metrics: Arc<dyn MetricsCollector>,
175    recv_backoff: RetryPolicy,
176    name: String,
177}
178
179impl<T: Send + Sync + Clone + 'static> ManagedConsumerBuilder<T> {
180    /// Create a new builder wrapping the given consumer and handler.
181    pub fn new(
182        name: impl Into<String>,
183        inner: Arc<dyn MessageConsumer<T>>,
184        handler: Arc<dyn MessageHandler<T>>,
185    ) -> Self {
186        Self {
187            inner,
188            handler,
189            metrics: Arc::new(NoopMetrics),
190            recv_backoff: RetryPolicy::new()
191                .with_linear_backoff(LinearBackoff::new(
192                    std::time::Duration::from_millis(500),
193                    std::time::Duration::from_millis(500),
194                    std::time::Duration::from_secs(5),
195                ))
196                .with_jitter(false),
197            name: name.into(),
198        }
199    }
200
201    /// Set the metrics collector.
202    #[must_use]
203    pub fn with_metrics(mut self, metrics: Arc<dyn MetricsCollector>) -> Self {
204        self.metrics = metrics;
205        self
206    }
207
208    /// Set the retry/backoff policy used after receive errors.
209    #[must_use]
210    pub fn with_recv_backoff(mut self, recv_backoff: RetryPolicy) -> Self {
211        self.recv_backoff = recv_backoff;
212        self
213    }
214
215    /// Build the managed consumer.
216    pub fn build(self) -> ManagedConsumer<T> {
217        ManagedConsumer {
218            inner: self.inner,
219            handler: self.handler,
220            metrics: self.metrics,
221            recv_backoff: self.recv_backoff,
222            name: self.name,
223            running: Arc::new(AtomicBool::new(false)),
224            task: parking_lot::Mutex::new(None),
225        }
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use std::sync::atomic::AtomicU32;
232
233    use super::*;
234    use crate::handler::FnHandler;
235    use crate::memory::InMemoryBroker;
236    use crate::message::Message;
237    use crate::traits::MessageProducer;
238
239    #[tokio::test]
240    async fn consumer_receives_and_handles_messages() {
241        let broker = InMemoryBroker::<String>::new(16);
242        let producer = broker.producer();
243        let consumer = broker.consumer();
244
245        // Subscribe before building managed consumer
246        crate::traits::MessageConsumer::subscribe(&consumer, &["test"])
247            .await
248            .unwrap();
249
250        let counter = Arc::new(AtomicU32::new(0));
251        let c = counter.clone();
252        let handler: Arc<dyn MessageHandler<String>> =
253            Arc::new(FnHandler::new(move |_msg: Message<String>| {
254                let c = c.clone();
255                async move {
256                    c.fetch_add(1, Ordering::SeqCst);
257                    Ok(())
258                }
259            }));
260
261        let managed = ManagedConsumerBuilder::new("test", Arc::new(consumer), handler).build();
262
263        managed.start().unwrap();
264
265        // Send a message
266        producer
267            .send(Message::new("test", "hello".to_string()))
268            .await
269            .unwrap();
270
271        // Give the consumer loop time to process
272        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
273
274        assert_eq!(counter.load(Ordering::SeqCst), 1);
275        assert!(managed.is_running());
276
277        managed.stop().await.unwrap();
278        assert!(!managed.is_running());
279    }
280
281    #[tokio::test]
282    async fn double_start_returns_error() {
283        let broker = InMemoryBroker::<String>::new(16);
284        let consumer = broker.consumer();
285
286        let handler: Arc<dyn MessageHandler<String>> =
287            Arc::new(FnHandler::new(|_msg: Message<String>| async { Ok(()) }));
288
289        let managed = ManagedConsumerBuilder::new("test", Arc::new(consumer), handler).build();
290
291        managed.start().unwrap();
292        assert!(managed.start().is_err());
293
294        managed.stop().await.unwrap();
295    }
296
297    #[tokio::test]
298    async fn stop_when_not_running_returns_error() {
299        let broker = InMemoryBroker::<String>::new(16);
300        let consumer = broker.consumer();
301
302        let handler: Arc<dyn MessageHandler<String>> =
303            Arc::new(FnHandler::new(|_msg: Message<String>| async { Ok(()) }));
304
305        let managed = ManagedConsumerBuilder::new("test", Arc::new(consumer), handler).build();
306
307        assert!(managed.stop().await.is_err());
308    }
309}