rocketmq_client_rust/implementation/
mq_client_api_factory.rs1use 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, ¬ify, 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}