Skip to main content

rocketmq_client_rust/implementation/
mq_client_api_factory.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;
19
20use crate::ClientError;
21use crate::ClientResult;
22use cheetah_string::CheetahString;
23use rand::RngExt;
24use serde::Serialize;
25use tokio::sync::Notify;
26use tokio::time::Duration;
27use tokio::time::Instant;
28
29use crate::common::nameserver_access_config::NameserverAccessConfig;
30use crate::implementation::mq_client_api_impl::MQClientAPIImpl;
31use crate::runtime::spawn_client_tracked_task_with_context;
32use crate::runtime::ClientTrackedTaskHandle;
33use rocketmq_runtime::ChildServiceContext;
34
35pub struct MQClientAPIFactory {
36    service_context: ChildServiceContext,
37    nameserver_access_config: NameserverAccessConfig,
38    name_prefix: CheetahString,
39    clients: Vec<Arc<MQClientAPIImpl>>,
40    namesrv_refresh_task: Option<NamesrvRefreshTaskHandle>,
41}
42
43enum NamesrvRefreshTaskHandle {
44    Tracked {
45        stop_signal: Arc<AtomicBool>,
46        stop_notify: Arc<Notify>,
47        handle: ClientTrackedTaskHandle,
48    },
49}
50
51impl NamesrvRefreshTaskHandle {
52    fn shutdown(self, timeout: Duration) -> bool {
53        match self {
54            Self::Tracked {
55                stop_signal,
56                stop_notify,
57                handle,
58            } => {
59                stop_signal.store(true, Ordering::Release);
60                stop_notify.notify_waiters();
61                let (report, completed) = handle.shutdown_blocking(timeout);
62                if !report.is_healthy() {
63                    tracing::warn!(
64                        report = %report.to_json(),
65                        "nameserver domain refresh task shutdown report is unhealthy"
66                    );
67                }
68                completed && report.is_healthy()
69            }
70        }
71    }
72
73    fn is_finished(&self) -> bool {
74        match self {
75            Self::Tracked { handle, .. } => handle.is_finished(),
76        }
77    }
78
79    fn task_count(&self) -> usize {
80        match self {
81            Self::Tracked { handle, .. } => handle.task_count(),
82        }
83    }
84}
85
86#[derive(Debug, Clone, Serialize)]
87pub struct NamesrvRefreshLifecycleProbe {
88    pub healthy: bool,
89    pub task_count_before_shutdown: usize,
90    pub task_count_after_shutdown: usize,
91    pub shutdown_elapsed_us: u128,
92}
93
94impl MQClientAPIFactory {
95    const NAMESRV_DOMAIN_FETCH_INITIAL_DELAY: Duration = Duration::from_secs(10);
96    const NAMESRV_DOMAIN_FETCH_INTERVAL: Duration = Duration::from_secs(120);
97    const NAMESRV_REFRESH_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
98
99    pub fn new(
100        service_context: ChildServiceContext,
101        nameserver_access_config: NameserverAccessConfig,
102        name_prefix: impl Into<CheetahString>,
103        clients: Vec<Arc<MQClientAPIImpl>>,
104    ) -> ClientResult<Self> {
105        validate_nameserver_access_config(&nameserver_access_config)?;
106        if clients.is_empty() {
107            return Err(ClientError::illegal_argument(
108                "MQClientAPIFactory requires at least one MQClientAPIImpl",
109            ));
110        }
111
112        Ok(Self {
113            service_context,
114            nameserver_access_config,
115            name_prefix: name_prefix.into(),
116            clients,
117            namesrv_refresh_task: None,
118        })
119    }
120
121    #[inline]
122    pub fn nameserver_access_config(&self) -> &NameserverAccessConfig {
123        &self.nameserver_access_config
124    }
125
126    #[inline]
127    pub fn name_prefix(&self) -> &CheetahString {
128        &self.name_prefix
129    }
130
131    #[inline]
132    pub fn client_num(&self) -> usize {
133        self.clients.len()
134    }
135
136    #[inline]
137    pub fn get_clients(&self) -> &[Arc<MQClientAPIImpl>] {
138        &self.clients
139    }
140
141    pub fn get_client(&self) -> ClientResult<Arc<MQClientAPIImpl>> {
142        match self.clients.len() {
143            0 => Err(ClientError::not_initialized("MQClientAPIFactory clients")),
144            1 => Ok(self.clients[0].clone()),
145            len => {
146                let index = rand::rng().random_range(0..len);
147                Ok(self.clients[index].clone())
148            }
149        }
150    }
151
152    pub async fn create_and_start(
153        service_context: ChildServiceContext,
154        nameserver_access_config: NameserverAccessConfig,
155        name_prefix: impl Into<CheetahString>,
156        clients: Vec<Arc<MQClientAPIImpl>>,
157    ) -> ClientResult<Self> {
158        let mut factory = Self::new(service_context, nameserver_access_config, name_prefix, clients)?;
159        factory.start().await?;
160        Ok(factory)
161    }
162
163    pub async fn start(&mut self) -> ClientResult<()> {
164        self.apply_nameserver_access_config().await?;
165        self.start_nameserver_domain_refresh();
166        for client in &self.clients {
167            client.start().await?;
168        }
169        Ok(())
170    }
171
172    pub fn shutdown(&mut self) {
173        self.stop_nameserver_domain_refresh();
174        for client in &self.clients {
175            client.shutdown();
176        }
177    }
178
179    pub async fn on_name_server_address_change(&mut self, namesrv_address: impl Into<String>) {
180        let namesrv_address = namesrv_address.into();
181        for client in &self.clients {
182            client
183                .on_name_server_address_change(Some(namesrv_address.clone()))
184                .await;
185        }
186    }
187
188    pub async fn apply_nameserver_access_config(&mut self) -> ClientResult<()> {
189        validate_nameserver_access_config(&self.nameserver_access_config)?;
190        if self.nameserver_access_config.has_namesrv_domain() {
191            for client in &self.clients {
192                client
193                    .configure_top_addressing(
194                        self.nameserver_access_config.namesrv_domain().as_str(),
195                        self.nameserver_access_config.namesrv_domain_subgroup().as_str(),
196                    )
197                    .await;
198                client.fetch_name_server_addr().await;
199            }
200            return Ok(());
201        }
202
203        let namesrv_addr = self.nameserver_access_config.namesrv_addr().as_str();
204        for client in &self.clients {
205            client.update_name_server_address_list(namesrv_addr).await;
206        }
207        Ok(())
208    }
209
210    fn start_nameserver_domain_refresh(&mut self) {
211        self.stop_nameserver_domain_refresh();
212        if !self.nameserver_access_config.has_namesrv_domain() {
213            return;
214        }
215
216        let clients = self.clients.clone();
217        let stop_signal = Arc::new(AtomicBool::new(false));
218        let stop_notify = Arc::new(Notify::new());
219        self.namesrv_refresh_task = spawn_namesrv_refresh_task(
220            &self.service_context,
221            "rocketmq-client-namesrv-refresh",
222            stop_signal.clone(),
223            stop_notify.clone(),
224            async move {
225                if !sleep_or_stop(&stop_signal, &stop_notify, Self::NAMESRV_DOMAIN_FETCH_INITIAL_DELAY).await {
226                    return;
227                }
228
229                loop {
230                    for client in &clients {
231                        client.fetch_name_server_addr().await;
232                    }
233
234                    if !sleep_or_stop(&stop_signal, &stop_notify, Self::NAMESRV_DOMAIN_FETCH_INTERVAL).await {
235                        break;
236                    }
237                }
238            },
239        );
240    }
241
242    fn stop_nameserver_domain_refresh(&mut self) {
243        if let Some(task) = self.namesrv_refresh_task.take() {
244            if !task.shutdown(Self::NAMESRV_REFRESH_SHUTDOWN_TIMEOUT) {
245                tracing::warn!("nameserver domain refresh task did not stop before timeout; aborted");
246            }
247        }
248    }
249
250    fn namesrv_refresh_task_count(&self) -> usize {
251        self.namesrv_refresh_task
252            .as_ref()
253            .map(NamesrvRefreshTaskHandle::task_count)
254            .unwrap_or_default()
255    }
256}
257
258fn validate_nameserver_access_config(config: &NameserverAccessConfig) -> ClientResult<()> {
259    if config.namesrv_domain().is_empty() && config.namesrv_addr().is_empty() {
260        return Err(ClientError::illegal_argument(
261            "The configuration item NamesrvAddr is not configured",
262        ));
263    }
264    Ok(())
265}
266
267fn spawn_namesrv_refresh_task<F>(
268    service_context: &ChildServiceContext,
269    thread_name: &'static str,
270    stop_signal: Arc<AtomicBool>,
271    stop_notify: Arc<Notify>,
272    task: F,
273) -> Option<NamesrvRefreshTaskHandle>
274where
275    F: Future<Output = ()> + Send + 'static,
276{
277    match spawn_client_tracked_task_with_context(service_context, thread_name, Box::pin(task)) {
278        Ok(handle) => Some(NamesrvRefreshTaskHandle::Tracked {
279            stop_signal,
280            stop_notify,
281            handle,
282        }),
283        Err(error) => {
284            tracing::warn!("Failed to spawn {} background task: {}", thread_name, error);
285            None
286        }
287    }
288}
289
290async fn sleep_or_stop(stop_signal: &Arc<AtomicBool>, stop_notify: &Arc<Notify>, delay: Duration) -> bool {
291    if stop_signal.load(Ordering::Acquire) {
292        return false;
293    }
294
295    tokio::time::timeout(delay, stop_notify.notified()).await.is_err() && !stop_signal.load(Ordering::Acquire)
296}
297
298#[doc(hidden)]
299pub async fn run_namesrv_refresh_lifecycle_probe(service_context: ChildServiceContext) -> NamesrvRefreshLifecycleProbe {
300    let mut factory = MQClientAPIFactory {
301        service_context,
302        nameserver_access_config: NameserverAccessConfig::new("", "example.com", "default"),
303        name_prefix: CheetahString::from_static_str("factory-lifecycle-probe"),
304        clients: Vec::new(),
305        namesrv_refresh_task: None,
306    };
307
308    factory.start_nameserver_domain_refresh();
309    let task_count_before_shutdown = factory.namesrv_refresh_task_count();
310
311    let shutdown_started = Instant::now();
312    factory.stop_nameserver_domain_refresh();
313    let shutdown_elapsed_us = shutdown_started.elapsed().as_micros();
314    let task_count_after_shutdown = factory.namesrv_refresh_task_count();
315
316    NamesrvRefreshLifecycleProbe {
317        healthy: task_count_before_shutdown == 1 && task_count_after_shutdown == 0,
318        task_count_before_shutdown,
319        task_count_after_shutdown,
320        shutdown_elapsed_us,
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use crate::test_support::error_assertions::assert_invalid_argument;
328    use std::future::pending;
329
330    struct DropFlag(Arc<AtomicBool>);
331
332    impl Drop for DropFlag {
333        fn drop(&mut self) {
334            self.0.store(true, Ordering::Release);
335        }
336    }
337
338    fn test_context() -> ChildServiceContext {
339        crate::runtime::test_service_context("mq-client-api-factory-test")
340    }
341
342    #[test]
343    fn factory_rejects_missing_nameserver_like_java() {
344        let error = validate_nameserver_access_config(&NameserverAccessConfig::default())
345            .expect_err("missing namesrvAddr and namesrvDomain should be invalid");
346
347        assert_invalid_argument(&error);
348    }
349
350    #[test]
351    fn factory_accepts_static_nameserver_address() {
352        validate_nameserver_access_config(&NameserverAccessConfig::new("127.0.0.1:9876", "", ""))
353            .expect("static namesrvAddr should be valid");
354    }
355
356    #[test]
357    fn factory_accepts_nameserver_domain() {
358        validate_nameserver_access_config(&NameserverAccessConfig::new("", "domain", "subgroup"))
359            .expect("namesrvDomain should be valid");
360    }
361
362    #[tokio::test]
363    async fn create_and_start_validates_before_starting() {
364        let result = MQClientAPIFactory::create_and_start(
365            test_context(),
366            NameserverAccessConfig::new("127.0.0.1:9876", "", ""),
367            "factory-test",
368            Vec::new(),
369        )
370        .await;
371        let error = match result {
372            Ok(_) => panic!("empty client list should be rejected before start"),
373            Err(error) => error,
374        };
375
376        assert_invalid_argument(&error);
377    }
378
379    #[tokio::test]
380    async fn domain_nameserver_config_starts_periodic_refresh_like_java() {
381        let mut factory = MQClientAPIFactory {
382            service_context: test_context(),
383            nameserver_access_config: NameserverAccessConfig::new("", "example.com", "default"),
384            name_prefix: CheetahString::from_static_str("factory-test"),
385            clients: Vec::new(),
386            namesrv_refresh_task: None,
387        };
388
389        factory.start_nameserver_domain_refresh();
390
391        assert!(factory
392            .namesrv_refresh_task
393            .as_ref()
394            .is_some_and(|task| !task.is_finished()));
395
396        factory.shutdown();
397
398        assert!(factory.namesrv_refresh_task.is_none());
399    }
400
401    #[tokio::test]
402    async fn static_nameserver_config_does_not_start_periodic_refresh() {
403        let mut factory = MQClientAPIFactory {
404            service_context: test_context(),
405            nameserver_access_config: NameserverAccessConfig::new("127.0.0.1:9876", "", ""),
406            name_prefix: CheetahString::from_static_str("factory-test"),
407            clients: Vec::new(),
408            namesrv_refresh_task: None,
409        };
410
411        factory.start_nameserver_domain_refresh();
412
413        assert!(factory.namesrv_refresh_task.is_none());
414    }
415
416    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
417    async fn namesrv_refresh_task_shutdown_waits_for_worker_completion() {
418        let stop_signal = Arc::new(AtomicBool::new(false));
419        let stop_notify = Arc::new(Notify::new());
420        let completed = Arc::new(AtomicBool::new(false));
421        let completed_in_task = completed.clone();
422        let stop_signal_in_task = stop_signal.clone();
423        let stop_notify_in_task = stop_notify.clone();
424        let task = spawn_namesrv_refresh_task(
425            &test_context(),
426            "rocketmq-client-namesrv-refresh-test",
427            stop_signal.clone(),
428            stop_notify.clone(),
429            async move {
430                let _ = sleep_or_stop(&stop_signal_in_task, &stop_notify_in_task, Duration::from_secs(60)).await;
431                completed_in_task.store(true, Ordering::Release);
432            },
433        )
434        .expect("test task should spawn");
435
436        assert!(task.shutdown(Duration::from_secs(1)));
437        assert!(completed.load(Ordering::Acquire));
438    }
439
440    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
441    async fn namesrv_refresh_task_shutdown_aborts_after_timeout() {
442        let stop_signal = Arc::new(AtomicBool::new(false));
443        let stop_notify = Arc::new(Notify::new());
444        let dropped = Arc::new(AtomicBool::new(false));
445        let dropped_in_task = dropped.clone();
446        let task = spawn_namesrv_refresh_task(
447            &test_context(),
448            "rocketmq-client-namesrv-refresh-test",
449            stop_signal,
450            stop_notify,
451            async move {
452                let _drop_flag = DropFlag(dropped_in_task);
453                pending::<()>().await;
454            },
455        )
456        .expect("test task should spawn");
457
458        assert!(!task.shutdown(Duration::from_millis(20)));
459
460        tokio::time::timeout(Duration::from_secs(1), async {
461            while !dropped.load(Ordering::Acquire) {
462                tokio::time::sleep(Duration::from_millis(5)).await;
463            }
464        })
465        .await
466        .expect("aborted task should be dropped");
467    }
468
469    #[tokio::test]
470    async fn sleep_or_stop_wakes_on_notify() {
471        let stop_signal = Arc::new(AtomicBool::new(false));
472        let stop_notify = Arc::new(Notify::new());
473        let signal = stop_signal.clone();
474        let notify = stop_notify.clone();
475
476        let wait = tokio::spawn(async move { sleep_or_stop(&signal, &notify, Duration::from_secs(60)).await });
477
478        stop_signal.store(true, Ordering::Release);
479        stop_notify.notify_waiters();
480
481        assert!(!tokio::time::timeout(Duration::from_secs(1), wait)
482            .await
483            .expect("sleep_or_stop should wake promptly")
484            .expect("sleep task should complete"));
485    }
486
487    #[tokio::test]
488    async fn namesrv_refresh_lifecycle_probe_reports_clean_shutdown() {
489        let probe = run_namesrv_refresh_lifecycle_probe(test_context()).await;
490
491        assert!(probe.healthy, "{probe:?}");
492        assert_eq!(probe.task_count_before_shutdown, 1);
493        assert_eq!(probe.task_count_after_shutdown, 0);
494    }
495
496    #[test]
497    fn domain_refresh_uses_injected_runtime_context() {
498        let mut factory = MQClientAPIFactory {
499            service_context: test_context(),
500            nameserver_access_config: NameserverAccessConfig::new("", "example.com", "default"),
501            name_prefix: CheetahString::from_static_str("factory-test"),
502            clients: Vec::new(),
503            namesrv_refresh_task: None,
504        };
505
506        factory.start_nameserver_domain_refresh();
507
508        assert!(factory
509            .namesrv_refresh_task
510            .as_ref()
511            .is_some_and(|task| !task.is_finished()));
512
513        factory.shutdown();
514
515        assert!(factory.namesrv_refresh_task.is_none());
516    }
517}