Skip to main content

rskit_messaging/
runner.rs

1//! Consumer runner that manages a consumption loop as a tokio task.
2
3use std::sync::Arc;
4use std::time::{Duration, Instant};
5
6use rskit_errors::{AppError, AppResult, ErrorCode};
7use rskit_stream::SpawnedTask;
8use tracing;
9
10use crate::handler::MessageHandler;
11use crate::metrics::{MetricsCollector, NoopMetrics};
12use crate::traits::MessageConsumer;
13
14/// Manages the consumption loop as a tokio task.
15///
16/// Unlike [`ManagedConsumer`](crate::managed_consumer::ManagedConsumer),
17/// `ConsumerRunner` owns the task handle directly and provides a simpler
18/// run/stop interface without builder overhead.
19pub struct ConsumerRunner<T: Send + Sync + Clone + 'static> {
20    consumer: Arc<dyn MessageConsumer<T>>,
21    handler: Arc<dyn MessageHandler<T>>,
22    metrics: Arc<dyn MetricsCollector>,
23    task: parking_lot::Mutex<Option<SpawnedTask>>,
24    shutdown_timeout: Duration,
25}
26
27impl<T: Send + Sync + Clone + 'static> ConsumerRunner<T> {
28    /// Create a new runner for the given consumer and handler.
29    pub fn new(consumer: Arc<dyn MessageConsumer<T>>, handler: Arc<dyn MessageHandler<T>>) -> Self {
30        Self {
31            consumer,
32            handler,
33            metrics: Arc::new(NoopMetrics),
34            task: parking_lot::Mutex::new(None),
35            shutdown_timeout: Duration::from_secs(10),
36        }
37    }
38
39    /// Set the metrics collector.
40    #[must_use]
41    pub fn with_metrics(mut self, metrics: Arc<dyn MetricsCollector>) -> Self {
42        self.metrics = metrics;
43        self
44    }
45
46    /// Set the graceful shutdown timeout.
47    #[must_use]
48    pub const fn with_shutdown_timeout(mut self, timeout: Duration) -> Self {
49        self.shutdown_timeout = timeout;
50        self
51    }
52
53    /// Returns `true` when the consumption loop is running.
54    pub fn is_running(&self) -> bool {
55        let guard = self.task.lock();
56        guard.as_ref().is_some_and(|t| !t.is_finished())
57    }
58
59    /// Start the consumption loop.
60    pub fn run(&self) -> AppResult<()> {
61        {
62            let guard = self.task.lock();
63            if guard.as_ref().is_some_and(|t| !t.is_finished()) {
64                return Err(AppError::new(
65                    ErrorCode::InvalidInput,
66                    "runner is already running",
67                ));
68            }
69        }
70
71        let consumer = self.consumer.clone();
72        let handler = self.handler.clone();
73        let metrics = self.metrics.clone();
74
75        let task = SpawnedTask::spawn(move |cancel| async move {
76            tracing::info!("consumer runner started");
77            loop {
78                tokio::select! {
79                    () = cancel.cancelled() => {
80                        tracing::info!("consumer runner cancelled");
81                        break;
82                    }
83                    result = consumer.recv() => {
84                        match result {
85                            Ok(msg) => {
86                                let topic = msg.topic.clone();
87                                let start = Instant::now();
88                                let handle_result = handler.handle(msg).await;
89                                metrics.record_consume(
90                                    &topic,
91                                    start.elapsed(),
92                                    handle_result.is_ok(),
93                                );
94                                if let Err(e) = handle_result {
95                                    tracing::warn!(error = %e, "handler error in runner");
96                                }
97                            }
98                            Err(e) => {
99                                if cancel.is_cancelled() {
100                                    break;
101                                }
102                                tracing::error!(error = %e, "recv error in runner");
103                                tokio::time::sleep(Duration::from_millis(100)).await;
104                            }
105                        }
106                    }
107                }
108            }
109            tracing::info!("consumer runner exited");
110        });
111
112        *self.task.lock() = Some(task);
113
114        Ok(())
115    }
116
117    /// Stop the consumption loop and wait for it to finish.
118    pub async fn stop(&self) -> AppResult<()> {
119        let task = {
120            let mut guard = self.task.lock();
121            guard.take()
122        };
123
124        let Some(task) = task else {
125            return Err(AppError::new(
126                ErrorCode::InvalidInput,
127                "runner is not running",
128            ));
129        };
130
131        task.shutdown(self.shutdown_timeout).await;
132        tracing::info!("consumer runner stopped");
133        Ok(())
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use std::sync::atomic::{AtomicU32, Ordering};
140
141    use super::*;
142    use crate::handler::FnHandler;
143    use crate::memory::InMemoryBroker;
144    use crate::message::Message;
145    use crate::traits::{self, MessageProducer};
146
147    #[tokio::test]
148    async fn runner_start_and_stop() {
149        let broker = InMemoryBroker::<String>::new(16);
150        let consumer = broker.consumer();
151
152        let handler: Arc<dyn MessageHandler<String>> =
153            Arc::new(FnHandler::new(|_msg: Message<String>| async { Ok(()) }));
154
155        let runner = ConsumerRunner::new(Arc::new(consumer), handler);
156
157        runner.run().unwrap();
158        assert!(runner.is_running());
159
160        runner.stop().await.unwrap();
161        // After stop the task has finished
162        assert!(!runner.is_running());
163    }
164
165    #[tokio::test]
166    async fn runner_processes_messages() {
167        let broker = InMemoryBroker::<String>::new(16);
168        let producer = broker.producer();
169        let consumer = broker.consumer();
170
171        traits::MessageConsumer::subscribe(&consumer, &["topic"])
172            .await
173            .unwrap();
174
175        let counter = Arc::new(AtomicU32::new(0));
176        let c = counter.clone();
177        let handler: Arc<dyn MessageHandler<String>> =
178            Arc::new(FnHandler::new(move |_msg: Message<String>| {
179                let c = c.clone();
180                async move {
181                    c.fetch_add(1, Ordering::SeqCst);
182                    Ok(())
183                }
184            }));
185
186        let runner = ConsumerRunner::new(Arc::new(consumer), handler);
187        runner.run().unwrap();
188
189        producer
190            .send(Message::new("topic", "msg1".to_string()))
191            .await
192            .unwrap();
193        producer
194            .send(Message::new("topic", "msg2".to_string()))
195            .await
196            .unwrap();
197
198        tokio::time::sleep(Duration::from_millis(150)).await;
199        assert_eq!(counter.load(Ordering::SeqCst), 2);
200
201        runner.stop().await.unwrap();
202    }
203
204    #[tokio::test]
205    async fn double_run_returns_error() {
206        let broker = InMemoryBroker::<String>::new(16);
207        let consumer = broker.consumer();
208
209        let handler: Arc<dyn MessageHandler<String>> =
210            Arc::new(FnHandler::new(|_msg: Message<String>| async { Ok(()) }));
211
212        let runner = ConsumerRunner::new(Arc::new(consumer), handler);
213
214        runner.run().unwrap();
215        assert!(runner.run().is_err());
216
217        runner.stop().await.unwrap();
218    }
219
220    #[tokio::test]
221    async fn stop_when_not_running_returns_error() {
222        let broker = InMemoryBroker::<String>::new(16);
223        let consumer = broker.consumer();
224
225        let handler: Arc<dyn MessageHandler<String>> =
226            Arc::new(FnHandler::new(|_msg: Message<String>| async { Ok(()) }));
227
228        let runner = ConsumerRunner::new(Arc::new(consumer), handler);
229        assert!(runner.stop().await.is_err());
230    }
231}