Skip to main content

rocketmq_client_rust/consumer/consumer_impl/
pull_message_service.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::future::Future;
16use std::sync::atomic::AtomicBool;
17use std::sync::atomic::Ordering;
18use std::sync::Arc;
19use std::thread;
20use std::time::Duration;
21
22use rocketmq_common::common::message::message_enum::MessageRequestMode;
23use rocketmq_error::RocketMQError;
24use rocketmq_rust::ArcMut;
25use rocketmq_rust::Shutdown;
26use tracing::error;
27use tracing::info;
28use tracing::warn;
29
30use crate::consumer::consumer_impl::message_request::MessageRequest;
31use crate::consumer::consumer_impl::pop_request::PopRequest;
32use crate::consumer::consumer_impl::pull_request::PullRequest;
33use crate::factory::mq_client_instance::MQClientInstance;
34
35/// Default queue capacity for message requests
36const DEFAULT_QUEUE_CAPACITY: usize = 4096;
37
38/// Default shutdown timeout in milliseconds
39const DEFAULT_SHUTDOWN_TIMEOUT_MS: u64 = 1000;
40
41/// RocketMQ Consumer Pull Message Service
42///
43/// # Responsibilities
44/// - Asynchronously schedules Pull/Pop message requests
45/// - Supports both delayed and immediate scheduling
46/// - Manages lifecycle of background pull tasks
47///
48/// # Thread Model
49/// - Main loop: Single Tokio task when a runtime is available
50/// - Delayed tasks: Tokio tasks, with a current-thread runtime fallback for synchronous callers
51///
52/// # Shutdown Semantics
53/// - `shutdown()` sends stop signal and waits for graceful termination
54/// - Main loop processes current request before exiting
55/// - Delayed tasks check `is_stopped()` before execution
56#[derive(Clone)]
57pub struct PullMessageService {
58    /// Message request channel sender
59    tx: Option<tokio::sync::mpsc::Sender<Box<dyn MessageRequest + Send + 'static>>>,
60
61    /// Shutdown signal broadcaster
62    tx_shutdown: Option<tokio::sync::broadcast::Sender<()>>,
63
64    /// Service stopped flag (for fast check)
65    stopped: Arc<AtomicBool>,
66
67    /// Queue capacity
68    queue_capacity: usize,
69}
70
71impl PullMessageService {
72    /// Creates a new PullMessageService instance
73    pub fn new() -> Self {
74        Self::with_capacity(DEFAULT_QUEUE_CAPACITY)
75    }
76
77    /// Creates a new PullMessageService instance with custom queue capacity
78    pub fn with_capacity(queue_capacity: usize) -> Self {
79        PullMessageService {
80            tx: None,
81            tx_shutdown: None,
82            stopped: Arc::new(AtomicBool::new(false)),
83            queue_capacity,
84        }
85    }
86
87    /// Checks if the service is stopped
88    #[inline]
89    pub fn is_stopped(&self) -> bool {
90        self.stopped.load(Ordering::Acquire)
91    }
92
93    /// Gets service name
94    #[inline]
95    pub fn get_service_name(&self) -> &'static str {
96        "PullMessageService"
97    }
98
99    /// Starts the pull message service
100    ///
101    /// # Arguments
102    /// * `instance` - MQClientInstance for message processing
103    ///
104    /// # Errors
105    /// Returns error if service is already started
106    pub async fn start(&mut self, mut instance: ArcMut<MQClientInstance>) -> Result<(), RocketMQError> {
107        if self.tx.is_some() {
108            warn!("{} already started", self.get_service_name());
109            return Ok(());
110        }
111
112        let (tx, mut rx) = tokio::sync::mpsc::channel::<Box<dyn MessageRequest + Send + 'static>>(self.queue_capacity);
113        let (mut shutdown, tx_shutdown) = Shutdown::new(1);
114
115        self.tx = Some(tx);
116        self.tx_shutdown = Some(tx_shutdown);
117        self.stopped.store(false, Ordering::Release);
118
119        spawn_pull_message_task("rocketmq-client-pull-message-service", async move {
120            info!("{} service started", "PullMessageService");
121
122            loop {
123                tokio::select! {
124                    _ = shutdown.recv() => {
125                        info!("{} received shutdown signal", "PullMessageService");
126                        break;
127                    }
128                    Some(request) = rx.recv() => {
129                        // Process request with exception handling
130                        if let Err(e) = Self::process_request(request, &mut instance).await {
131                            error!("{} failed to process request: {:?}", "PullMessageService", e);
132                        }
133                    }
134                }
135            }
136
137            info!("{} service end", "PullMessageService");
138        });
139
140        Ok(())
141    }
142
143    /// Processes a message request (Pull or Pop)
144    ///
145    /// # Arguments
146    /// * `request` - Message request to process
147    /// * `instance` - MQClientInstance for consumer lookup
148    ///
149    /// # Errors
150    /// Returns error if request processing fails
151    async fn process_request(
152        request: Box<dyn MessageRequest + Send + 'static>,
153        instance: &mut MQClientInstance,
154    ) -> Result<(), RocketMQError> {
155        match request.get_message_request_mode() {
156            MessageRequestMode::Pull => {
157                // Safe downcast using Any trait
158                if let Ok(pull_request) = request.into_any().downcast::<PullRequest>() {
159                    Self::pull_message(*pull_request, instance).await;
160                    Ok(())
161                } else {
162                    Err(RocketMQError::Internal("Failed to downcast to PullRequest".to_string()))
163                }
164            }
165            MessageRequestMode::Pop => {
166                if let Ok(pop_request) = request.into_any().downcast::<PopRequest>() {
167                    Self::pop_message(*pop_request, instance).await;
168                    Ok(())
169                } else {
170                    Err(RocketMQError::Internal("Failed to downcast to PopRequest".to_string()))
171                }
172            }
173        }
174    }
175
176    /// Handles pull message request
177    async fn pull_message(request: PullRequest, instance: &mut MQClientInstance) {
178        if let Some(mut consumer) = instance.select_consumer(request.get_consumer_group()).await {
179            consumer.pull_message(request).await;
180        } else {
181            warn!("No matched consumer for the PullRequest {}, drop it", request)
182        }
183    }
184
185    /// Handles pop message request
186    async fn pop_message(request: PopRequest, instance: &mut MQClientInstance) {
187        if let Some(mut consumer) = instance.select_consumer(request.get_consumer_group()).await {
188            consumer.pop_message(request).await;
189        } else {
190            warn!("No matched consumer for the PopRequest {}, drop it", request)
191        }
192    }
193
194    /// Executes pull request with delay
195    ///
196    /// # Arguments
197    /// * `pull_request` - The pull request to execute
198    /// * `time_delay` - Delay in milliseconds before execution
199    ///
200    /// # Behavior
201    /// - Returns immediately if service is stopped
202    /// - Spawns a tokio task that sleeps then sends the request
203    pub fn execute_pull_request_later(&self, pull_request: PullRequest, time_delay: u64) {
204        if self.is_stopped() {
205            warn!("{} has shutdown, cannot execute later task", self.get_service_name());
206            return;
207        }
208
209        let this = self.clone();
210        let request = pull_request.clone();
211
212        // Use a one-shot scheduled task
213        spawn_pull_message_task("rocketmq-client-pull-request-delay", async move {
214            tokio::time::sleep(Duration::from_millis(time_delay)).await;
215
216            if this.is_stopped() {
217                return;
218            }
219
220            if let Some(tx) = &this.tx {
221                if let Err(e) = tx.send(Box::new(request)).await {
222                    warn!("Failed to send pull request: {:?}", e);
223                }
224            }
225        });
226    }
227
228    /// Executes pull request immediately
229    ///
230    /// # Arguments
231    /// * `pull_request` - The pull request to execute
232    ///
233    /// # Behavior
234    /// Logs error but does not return error (aligned with Java implementation)
235    pub async fn execute_pull_request_immediately(&self, pull_request: PullRequest) {
236        if self.is_stopped() {
237            warn!("PullMessageService has shutdown");
238            return;
239        }
240
241        if let Some(tx) = &self.tx {
242            if let Err(e) = tx.send(Box::new(pull_request)).await {
243                error!("executePullRequestImmediately messageRequestQueue.put error: {:?}", e);
244            }
245        } else {
246            warn!("PullMessageService not started");
247        }
248    }
249
250    /// Executes pop request with delay
251    pub fn execute_pop_pull_request_later(&self, pop_request: PopRequest, time_delay: u64) {
252        if self.is_stopped() {
253            warn!("{} has shutdown, cannot execute later task", self.get_service_name());
254            return;
255        }
256
257        let this = self.clone();
258        let request = pop_request.clone();
259
260        // Use a one-shot scheduled task
261        spawn_pull_message_task("rocketmq-client-pop-request-delay", async move {
262            tokio::time::sleep(Duration::from_millis(time_delay)).await;
263
264            if this.is_stopped() {
265                return;
266            }
267
268            if let Some(tx) = &this.tx {
269                if let Err(e) = tx.send(Box::new(request)).await {
270                    warn!("Failed to send pop request: {:?}", e);
271                }
272            }
273        });
274    }
275
276    /// Executes pop request immediately
277    pub async fn execute_pop_pull_request_immediately(&self, pop_request: PopRequest) {
278        if self.is_stopped() {
279            warn!("PullMessageService has shutdown");
280            return;
281        }
282
283        if let Some(tx) = &self.tx {
284            if let Err(e) = tx.send(Box::new(pop_request)).await {
285                error!(
286                    "executePopPullRequestImmediately messageRequestQueue.put error: {:?}",
287                    e
288                );
289            }
290        } else {
291            warn!("PullMessageService not started");
292        }
293    }
294
295    /// Executes a generic task with delay (equivalent to Java's executeTaskLater)
296    ///
297    /// # Arguments
298    /// * `task` - Task function to execute
299    /// * `time_delay` - Delay in milliseconds before execution
300    pub fn execute_task_later<F>(&self, task: F, time_delay: u64)
301    where
302        F: FnOnce() + Send + 'static,
303    {
304        if self.is_stopped() {
305            warn!("{} has shutdown, cannot execute task", self.get_service_name());
306            return;
307        }
308
309        let stopped = self.stopped.clone();
310        spawn_pull_message_task("rocketmq-client-pull-task-delay", async move {
311            tokio::time::sleep(Duration::from_millis(time_delay)).await;
312            if stopped.load(Ordering::Acquire) {
313                return;
314            }
315            task();
316        });
317    }
318
319    /// Executes a generic task immediately (equivalent to Java's executeTask)
320    pub fn execute_task<F>(&self, task: F)
321    where
322        F: FnOnce() + Send + 'static,
323    {
324        if self.is_stopped() {
325            warn!("{} has shutdown, cannot execute task", self.get_service_name());
326            return;
327        }
328
329        spawn_pull_message_task("rocketmq-client-pull-task", async move {
330            task();
331        });
332    }
333
334    /// Gracefully shuts down the service
335    ///
336    /// # Arguments
337    /// * `timeout_ms` - Maximum time to wait for shutdown (milliseconds)
338    ///
339    /// # Behavior
340    /// - Sets stopped flag
341    /// - Sends shutdown signal to main loop
342    /// - Cancels all scheduled tasks
343    /// - Waits for main loop to finish (with timeout)
344    pub async fn shutdown(&self, timeout_ms: u64) -> Result<(), RocketMQError> {
345        if self.is_stopped() {
346            warn!("{} already stopped", self.get_service_name());
347            return Ok(());
348        }
349
350        info!("{} shutting down...", self.get_service_name());
351
352        // 1. Set stopped flag
353        self.stopped.store(true, Ordering::Release);
354
355        // 2. Send shutdown signal
356        if let Some(tx_shutdown) = &self.tx_shutdown {
357            tx_shutdown
358                .send(())
359                .map_err(|_| RocketMQError::Internal("Failed to send shutdown signal".to_string()))?;
360        }
361
362        // 3. Wait for main loop to exit (with timeout)
363        let wait_result = tokio::time::timeout(Duration::from_millis(timeout_ms), async {
364            // Wait for tx to be dropped (main loop exited)
365            while self.tx.as_ref().map(|tx| !tx.is_closed()).unwrap_or(false) {
366                tokio::time::sleep(Duration::from_millis(10)).await;
367            }
368        })
369        .await;
370
371        if wait_result.is_err() {
372            warn!(
373                "{} shutdown timeout after {}ms, forcing exit",
374                self.get_service_name(),
375                timeout_ms
376            );
377        }
378
379        info!("{} shutdown completed", self.get_service_name());
380        Ok(())
381    }
382
383    /// Shuts down with default timeout
384    pub async fn shutdown_default(&self) -> Result<(), RocketMQError> {
385        self.shutdown(DEFAULT_SHUTDOWN_TIMEOUT_MS).await
386    }
387}
388
389impl Default for PullMessageService {
390    fn default() -> Self {
391        Self::new()
392    }
393}
394
395fn spawn_pull_message_task<F>(thread_name: &'static str, task: F)
396where
397    F: Future<Output = ()> + Send + 'static,
398{
399    if let Ok(handle) = tokio::runtime::Handle::try_current() {
400        drop(handle.spawn(task));
401        return;
402    }
403
404    match thread::Builder::new().name(thread_name.to_string()).spawn(move || {
405        match tokio::runtime::Builder::new_current_thread().enable_all().build() {
406            Ok(runtime) => runtime.block_on(task),
407            Err(error) => error!("Failed to build {} runtime: {}", thread_name, error),
408        }
409    }) {
410        Ok(_handle) => {}
411        Err(error) => error!("Failed to spawn {} background thread: {}", thread_name, error),
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418
419    #[test]
420    fn execute_task_without_tokio_runtime_does_not_spawn_panic() {
421        let service = PullMessageService::new();
422        let ran = Arc::new(AtomicBool::new(false));
423        let ran_clone = ran.clone();
424
425        service.execute_task(move || ran_clone.store(true, Ordering::Release));
426
427        std::thread::sleep(Duration::from_millis(50));
428        assert!(ran.load(Ordering::Acquire));
429    }
430
431    #[test]
432    fn execute_task_later_without_tokio_runtime_does_not_spawn_panic() {
433        let service = PullMessageService::new();
434        let ran = Arc::new(AtomicBool::new(false));
435        let ran_clone = ran.clone();
436
437        service.execute_task_later(move || ran_clone.store(true, Ordering::Release), 1);
438
439        std::thread::sleep(Duration::from_millis(50));
440        assert!(ran.load(Ordering::Acquire));
441    }
442}