1use std::any::Any;
16use std::collections::HashMap;
17use std::collections::HashSet;
18use std::future::Future;
19use std::sync::atomic::AtomicBool;
20use std::sync::atomic::AtomicU64;
21use std::sync::atomic::AtomicUsize;
22use std::sync::atomic::Ordering;
23use std::sync::Arc;
24use std::thread;
25use std::time::Duration;
26use std::time::Instant;
27
28use bytes::Bytes;
29use cheetah_string::CheetahString;
30use parking_lot::Mutex;
31use parking_lot::RwLock;
32use rocketmq_common::common::message::message_queue::MessageQueue;
33use rocketmq_common::common::message::message_single::Message;
34use rocketmq_common::common::topic::TopicValidator;
35use rocketmq_error::RocketMQError;
36use rocketmq_error::RocketMQResult;
37use rocketmq_remoting::runtime::RPCHook;
38use rocketmq_rust::ArcMut;
39use tokio::sync::mpsc;
40use tokio::task::JoinHandle;
41use tracing::debug;
42use tracing::error;
43use tracing::info;
44use tracing::warn;
45
46use crate::base::access_channel::AccessChannel;
47use crate::base::client_config::ClientConfig;
48use crate::consumer::consumer_impl::default_mq_push_consumer_impl::DefaultMQPushConsumerImpl;
49use crate::producer::default_mq_producer::DefaultMQProducer;
50use crate::producer::producer_impl::default_mq_producer_impl::DefaultMQProducerImpl;
51use crate::trace::trace_constants::TraceConstants;
52use crate::trace::trace_context::TraceContext;
53use crate::trace::trace_data_encoder::TraceDataEncoder;
54use crate::trace::trace_dispatcher::TraceDispatcher;
55use crate::trace::trace_dispatcher::Type;
56use crate::trace::trace_transfer_bean::TraceTransferBean;
57
58#[derive(Clone)]
60struct TraceDispatcherConfig {
61 group: String,
62 type_: Type,
63 batch_num: usize,
64 max_msg_size: usize,
65 flush_interval: Duration,
66 trace_topic_name: String,
67 rpc_hook: Option<Arc<dyn RPCHook>>,
68}
69
70struct DispatcherState {
72 is_started: AtomicBool,
73 is_stopped: AtomicBool,
74 discard_count: AtomicU64,
75 last_flush_time: AtomicU64,
76 send_which_queue: AtomicUsize,
77 access_channel: RwLock<AccessChannel>,
78 host_producer: RwLock<Option<ArcMut<DefaultMQProducerImpl>>>,
79 host_consumer: RwLock<Option<ArcMut<DefaultMQPushConsumerImpl>>>,
80 namespace_v2: RwLock<Option<CheetahString>>,
81}
82
83impl DispatcherState {
84 fn new() -> Self {
85 Self {
86 is_started: AtomicBool::new(false),
87 is_stopped: AtomicBool::new(false),
88 discard_count: AtomicU64::new(0),
89 last_flush_time: AtomicU64::new(0),
90 send_which_queue: AtomicUsize::new(0),
91 access_channel: RwLock::new(AccessChannel::Local),
92 host_producer: RwLock::new(None),
93 host_consumer: RwLock::new(None),
94 namespace_v2: RwLock::new(None),
95 }
96 }
97}
98
99enum TraceTaskHandle {
100 Tokio(JoinHandle<()>),
101 Thread(thread::JoinHandle<()>),
102}
103
104impl TraceTaskHandle {
105 fn abort(self) {
106 match self {
107 Self::Tokio(handle) => handle.abort(),
108 Self::Thread(handle) => drop(handle),
109 }
110 }
111}
112
113pub struct AsyncTraceDispatcher {
134 config: TraceDispatcherConfig,
135 state: Arc<DispatcherState>,
136 use_tls: AtomicBool,
137 tx: mpsc::Sender<TraceContext>,
138 rx: Mutex<Option<mpsc::Receiver<TraceContext>>>,
139 worker_handle: Mutex<Option<TraceTaskHandle>>,
140 trace_producer: RwLock<Option<Arc<tokio::sync::Mutex<DefaultMQProducer>>>>,
141 instance_counter: Arc<AtomicUsize>,
142}
143
144impl AsyncTraceDispatcher {
145 pub fn new(
158 group: &str,
159 type_: Type,
160 batch_num: usize,
161 trace_topic_name: &str,
162 rpc_hook: Option<Arc<dyn RPCHook>>,
163 ) -> Self {
164 let (tx, rx) = mpsc::channel(2048);
165
166 let trace_topic = if trace_topic_name.is_empty() {
167 TopicValidator::RMQ_SYS_TRACE_TOPIC.to_string()
168 } else {
169 trace_topic_name.to_string()
170 };
171
172 let config = TraceDispatcherConfig {
173 group: group.to_string(),
174 type_,
175 batch_num: batch_num.min(20),
176 max_msg_size: 128_000, flush_interval: Duration::from_secs(5),
178 trace_topic_name: trace_topic,
179 rpc_hook,
180 };
181
182 Self {
183 config,
184 state: Arc::new(DispatcherState::new()),
185 use_tls: AtomicBool::new(false),
186 tx,
187 rx: Mutex::new(Some(rx)),
188 worker_handle: Mutex::new(None),
189 trace_producer: RwLock::new(None),
190 instance_counter: Arc::new(AtomicUsize::new(0)),
191 }
192 }
193
194 fn gen_group_name(&self) -> String {
196 let counter = self.instance_counter.fetch_add(1, Ordering::SeqCst);
197 format!(
198 "{}-{}-{:?}-{}",
199 TraceConstants::GROUP_NAME_PREFIX,
200 self.config.group,
201 self.config.type_,
202 counter
203 )
204 }
205
206 pub fn get_discard_count(&self) -> u64 {
208 self.state.discard_count.load(Ordering::Relaxed)
209 }
210
211 pub fn queue_size(&self) -> usize {
217 0
220 }
221
222 pub fn is_started(&self) -> bool {
224 self.state.is_started.load(Ordering::SeqCst)
225 }
226
227 pub fn is_stopped(&self) -> bool {
229 self.state.is_stopped.load(Ordering::SeqCst)
230 }
231
232 pub fn batch_num(&self) -> usize {
234 self.config.batch_num
235 }
236
237 pub fn set_host_producer(&self, host_producer: ArcMut<DefaultMQProducerImpl>) {
241 *self.state.host_producer.write() = Some(host_producer);
242 }
243
244 pub fn set_host_consumer(&self, host_consumer: ArcMut<DefaultMQPushConsumerImpl>) {
248 *self.state.host_consumer.write() = Some(host_consumer);
249 }
250
251 pub fn set_namespace_v2(&self, namespace_v2: Option<CheetahString>) {
253 *self.state.namespace_v2.write() = namespace_v2;
254 }
255
256 pub fn set_use_tls(&self, use_tls: bool) {
258 self.use_tls.store(use_tls, Ordering::Release);
259 }
260
261 pub fn is_use_tls(&self) -> bool {
262 self.use_tls.load(Ordering::Acquire)
263 }
264
265 pub fn get_trace_producer(&self) -> Option<Arc<tokio::sync::Mutex<DefaultMQProducer>>> {
269 self.trace_producer.read().clone()
270 }
271
272 pub fn trace_topic_name(&self) -> &str {
273 self.config.trace_topic_name.as_str()
274 }
275
276 pub fn register_shutdown_hook(&self) {}
277
278 pub fn register_shut_down_hook(&self) {
279 self.register_shutdown_hook();
280 }
281
282 pub fn remove_shutdown_hook(&self) {}
283}
284
285impl TraceDispatcher for AsyncTraceDispatcher {
286 fn start(&self, name_srv_addr: &str, access_channel: AccessChannel) -> RocketMQResult<()> {
297 if self
299 .state
300 .is_started
301 .compare_exchange(false, true, Ordering::SeqCst, Ordering::Relaxed)
302 .is_err()
303 {
304 warn!("AsyncTraceDispatcher already started");
305 return Ok(());
306 }
307
308 info!("Starting AsyncTraceDispatcher...");
309
310 let producer_group = self.gen_group_name();
312
313 let mut client_config = ClientConfig::default();
315 client_config.set_namesrv_addr(CheetahString::from_string(name_srv_addr.to_string()));
316 client_config.set_enable_trace(false); client_config.set_vip_channel_enabled(false); client_config.set_use_tls(self.use_tls.load(Ordering::Acquire));
319
320 let mut builder = DefaultMQProducer::builder()
321 .producer_group(&producer_group)
322 .client_config(client_config)
323 .send_msg_timeout(5000)
324 .max_message_size(self.config.max_msg_size as u32);
325
326 if let Some(rpc_hook) = self.config.rpc_hook.clone() {
327 builder = builder.rpc_hook(rpc_hook);
328 }
329
330 let producer = builder.build();
331
332 info!("Trace producer initialized: group={}", producer_group);
333
334 *self.trace_producer.write() = Some(Arc::new(tokio::sync::Mutex::new(producer)));
336 *self.state.access_channel.write() = access_channel;
337
338 let rx = self
340 .rx
341 .lock()
342 .take()
343 .ok_or_else(|| RocketMQError::not_initialized("Dispatcher already started"))?;
344
345 let producer = self
347 .trace_producer
348 .read()
349 .clone()
350 .ok_or_else(|| RocketMQError::not_initialized("Producer not initialized"))?;
351
352 let state = self.state.clone();
354 let config = self.config.clone();
355
356 let handle = match spawn_trace_task("rocketmq-client-trace-worker", async move {
357 if let Err(e) = worker_loop(rx, state, producer, config).await {
358 error!("Worker loop failed: {:?}", e);
359 }
360 }) {
361 Ok(handle) => handle,
362 Err(error) => {
363 self.state.is_started.store(false, Ordering::SeqCst);
364 return Err(error);
365 }
366 };
367
368 *self.worker_handle.lock() = Some(handle);
369
370 info!("AsyncTraceDispatcher started successfully");
371 Ok(())
372 }
373
374 fn append(&self, ctx: &dyn Any) -> bool {
388 let trace_ctx = match ctx.downcast_ref::<TraceContext>() {
390 Some(ctx) => ctx,
391 None => {
392 warn!("Failed to downcast to TraceContext");
393 return false;
394 }
395 };
396
397 match self.tx.try_send(trace_ctx.clone()) {
399 Ok(_) => true,
400 Err(mpsc::error::TrySendError::Full(_)) => {
401 let count = self.state.discard_count.fetch_add(1, Ordering::Relaxed);
403 warn!(
404 "Trace context queue full (discard count: {}), data discarded",
405 count + 1
406 );
407 false
408 }
409 Err(mpsc::error::TrySendError::Closed(_)) => {
410 error!("Trace dispatcher channel closed");
411 false
412 }
413 }
414 }
415
416 fn flush(&self) -> RocketMQResult<()> {
426 if !self.state.is_started.load(Ordering::SeqCst) {
427 return Err(RocketMQError::not_initialized("Dispatcher not started"));
428 }
429
430 info!("Flushing trace data...");
431
432 std::thread::sleep(Duration::from_millis(100));
434
435 info!("Flush completed");
436 Ok(())
437 }
438
439 fn shutdown(&self) {
445 if !self.state.is_started.load(Ordering::SeqCst) {
446 warn!("AsyncTraceDispatcher not started");
447 return;
448 }
449
450 self.state.is_stopped.store(true, Ordering::SeqCst);
452
453 info!("Shutting down AsyncTraceDispatcher...");
454
455 if let Err(e) = self.flush() {
457 error!("Flush failed during shutdown: {:?}", e);
458 }
459
460 if let Some(handle) = self.worker_handle.lock().take() {
461 handle.abort();
462 }
463
464 if let Some(_producer) = self.trace_producer.read().as_ref() {
465 info!("Trace producer shut down");
466 }
467
468 self.state.is_started.store(false, Ordering::SeqCst);
470 *self.trace_producer.write() = None;
471
472 info!("AsyncTraceDispatcher stopped");
473 }
474
475 fn as_any(&self) -> &dyn Any {
476 self
477 }
478
479 fn as_mut_any(&mut self) -> &mut dyn Any {
480 self
481 }
482
483 fn trace_topic_name(&self) -> Option<&str> {
484 Some(self.config.trace_topic_name.as_str())
485 }
486
487 fn trace_client_host(&self) -> Option<CheetahString> {
488 self.state
489 .host_producer
490 .read()
491 .as_ref()
492 .and_then(|producer| producer.client_id())
493 }
494}
495
496impl Drop for AsyncTraceDispatcher {
497 fn drop(&mut self) {
498 if self.state.is_started.load(Ordering::SeqCst) && !self.state.is_stopped.load(Ordering::SeqCst) {
499 warn!("AsyncTraceDispatcher dropped without explicit shutdown, auto-flushing...");
500 if let Err(e) = self.flush() {
502 error!("Auto-flush on drop failed: {:?}", e);
503 }
504 self.shutdown();
505 }
506 }
507}
508
509fn spawn_trace_task<F>(thread_name: &'static str, task: F) -> RocketMQResult<TraceTaskHandle>
510where
511 F: Future<Output = ()> + Send + 'static,
512{
513 if let Ok(handle) = tokio::runtime::Handle::try_current() {
514 return Ok(TraceTaskHandle::Tokio(handle.spawn(task)));
515 }
516
517 match tokio::runtime::Builder::new_current_thread().enable_all().build() {
518 Ok(runtime) => thread::Builder::new()
519 .name(thread_name.to_string())
520 .spawn(move || runtime.block_on(task))
521 .map(TraceTaskHandle::Thread)
522 .map_err(|error| {
523 RocketMQError::Internal(format!("failed to spawn {thread_name} background thread: {error}"))
524 }),
525 Err(error) => Err(RocketMQError::Internal(format!(
526 "failed to build {thread_name} runtime: {error}"
527 ))),
528 }
529}
530
531async fn worker_loop(
534 mut rx: mpsc::Receiver<TraceContext>,
535 state: Arc<DispatcherState>,
536 producer: Arc<tokio::sync::Mutex<DefaultMQProducer>>,
537 config: TraceDispatcherConfig,
538) -> RocketMQResult<()> {
539 {
541 let mut producer_guard = producer.lock().await;
542 let start_result = producer_guard.start().await;
543 drop(producer_guard); if let Err(e) = start_result {
546 error!("Failed to start trace producer: {:?}", e);
547 return Err(e);
548 }
549 info!("Trace producer started successfully in worker");
550 }
551
552 let mut interval = tokio::time::interval(Duration::from_millis(5));
553 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
554
555 let mut buffer = Vec::with_capacity(config.batch_num);
556 let mut last_flush = Instant::now();
557
558 loop {
559 tokio::select! {
560 _ = interval.tick() => {
562 if !buffer.is_empty() {
563 let should_flush = buffer.len() >= config.batch_num
564 || last_flush.elapsed() >= config.flush_interval;
565
566 if should_flush {
567 flush_buffer(&mut buffer, &state, &producer, &config).await;
568 last_flush = Instant::now();
569 }
570 }
571
572 if state.is_stopped.load(Ordering::SeqCst) && buffer.is_empty() {
574 info!("Worker loop exiting (stopped flag set)");
575 break;
576 }
577 }
578
579 result = rx.recv() => {
581 match result {
582 Some(ctx) => {
583 buffer.push(ctx);
584
585 if buffer.len() >= config.batch_num {
587 flush_buffer(&mut buffer, &state, &producer, &config).await;
588 last_flush = Instant::now();
589 }
590 }
591 None => {
592 info!("Worker loop exiting (channel closed)");
594 if !buffer.is_empty() {
595 flush_buffer(&mut buffer, &state, &producer, &config).await;
596 }
597 break;
598 }
599 }
600 }
601 }
602 }
603
604 Ok(())
605}
606
607async fn flush_buffer(
609 buffer: &mut Vec<TraceContext>,
610 state: &Arc<DispatcherState>,
611 producer: &Arc<tokio::sync::Mutex<DefaultMQProducer>>,
612 config: &TraceDispatcherConfig,
613) {
614 if buffer.is_empty() {
615 return;
616 }
617
618 let contexts = std::mem::take(buffer);
620
621 if let Err(error) = send_trace_data(contexts, state, producer, config).await {
622 error!("send_trace_data failed: {:?}", error);
623 }
624}
625
626async fn send_trace_data(
629 contexts: Vec<TraceContext>,
630 state: &Arc<DispatcherState>,
631 producer: &Arc<tokio::sync::Mutex<DefaultMQProducer>>,
632 config: &TraceDispatcherConfig,
633) -> RocketMQResult<()> {
634 let mut trans_bean_map: HashMap<String, Vec<TraceTransferBean>> = HashMap::with_capacity(16);
636
637 let default_access_channel = *state.access_channel.read();
638
639 for ctx in contexts {
640 let access_channel = ctx.access_channel.unwrap_or(default_access_channel);
641
642 let region_id = &ctx.region_id;
643 if region_id.is_empty() {
644 continue;
645 }
646
647 let trace_beans = match &ctx.trace_beans {
648 Some(beans) if !beans.is_empty() => beans,
649 _ => continue,
650 };
651
652 let trace_topic = match access_channel {
654 AccessChannel::Cloud => {
655 format!("{}{}", TraceConstants::TRACE_TOPIC_PREFIX, region_id)
656 }
657 _ => config.trace_topic_name.clone(),
658 };
659
660 let topic = &trace_beans[0].topic;
661 let key = format!("{}{}{}", topic, TraceConstants::CONTENT_SPLITOR, trace_topic);
662
663 if let Some(transfer_bean) = TraceDataEncoder::encoder_from_context_bean(&ctx) {
665 trans_bean_map.entry(key).or_default().push(transfer_bean);
666 }
667 }
668
669 for (key, beans) in trans_bean_map {
671 let parts: Vec<&str> = key.split(TraceConstants::CONTENT_SPLITOR).collect();
672 if parts.len() != 2 {
673 warn!("Invalid trace key: {}", key);
674 continue;
675 }
676
677 let topic = parts[0];
678 let trace_topic = parts[1];
679
680 if let Err(e) = flush_data(beans, topic, trace_topic, producer, state, config).await {
681 error!(
682 "flush_data failed for topic={}, trace_topic={}: {:?}",
683 topic, trace_topic, e
684 );
685 }
686 }
687
688 Ok(())
689}
690
691async fn flush_data(
694 beans: Vec<TraceTransferBean>,
695 _topic: &str,
696 trace_topic: &str,
697 producer: &Arc<tokio::sync::Mutex<DefaultMQProducer>>,
698 state: &Arc<DispatcherState>,
699 config: &TraceDispatcherConfig,
700) -> RocketMQResult<()> {
701 if beans.is_empty() {
702 return Ok(());
703 }
704
705 let mut buffer = String::with_capacity(1024);
706 let mut key_set = HashSet::new();
707
708 for bean in beans {
709 key_set.extend(bean.trans_key().iter().cloned());
711
712 buffer.push_str(bean.trans_data());
714
715 if buffer.len() >= config.max_msg_size {
717 send_trace_message(&key_set, &buffer, trace_topic, producer, state).await?;
718 buffer.clear();
719 key_set.clear();
720 }
721 }
722
723 if !buffer.is_empty() {
725 send_trace_message(&key_set, &buffer, trace_topic, producer, state).await?;
726 }
727
728 Ok(())
729}
730
731async fn send_trace_message(
735 key_set: &HashSet<CheetahString>,
736 data: &str,
737 trace_topic: &str,
738 producer: &Arc<tokio::sync::Mutex<DefaultMQProducer>>,
739 state: &Arc<DispatcherState>,
740) -> RocketMQResult<()> {
741 let keys: Vec<String> = key_set.iter().map(|k| k.to_string()).collect();
742
743 let message = Message::builder()
744 .topic(trace_topic)
745 .body(Bytes::from(data.as_bytes().to_vec()))
746 .keys(keys)
747 .build()?;
748
749 debug!(
750 "Sending trace message: topic={}, size={}, keys={}",
751 trace_topic,
752 data.len(),
753 key_set.len()
754 );
755
756 let state_clone = state.clone();
759 let selector = move |queues: &[MessageQueue], _msg: &Message, _arg: &()| -> Option<MessageQueue> {
760 if queues.is_empty() {
761 return None;
762 }
763 let index = state_clone.send_which_queue.fetch_add(1, Ordering::Relaxed);
765 let pos = index % queues.len();
766 queues.get(pos).cloned()
767 };
768
769 let result = producer
772 .lock()
773 .await
774 .send_with_selector_timeout(message, selector, (), 5000)
775 .await?;
776 debug!(
777 "Trace message sent successfully: msgId={}, status={:?}",
778 result
779 .as_ref()
780 .and_then(|send_result| send_result.msg_id.as_ref())
781 .map_or("N/A", |id| id.as_str()),
782 result.as_ref().map(|send_result| send_result.send_status)
783 );
784
785 Ok(())
786}
787
788#[cfg(test)]
789mod tests {
790 use super::*;
791
792 #[test]
793 fn spawn_trace_task_without_tokio_runtime_runs_on_fallback_thread() {
794 let (tx, rx) = std::sync::mpsc::channel();
795
796 let handle = spawn_trace_task("rocketmq-client-trace-test", async move {
797 tx.send(std::thread::current().name().unwrap_or_default().to_string())
798 .expect("test receiver should be alive");
799 })
800 .expect("fallback trace runtime should start");
801
802 let thread_name = rx
803 .recv_timeout(Duration::from_secs(2))
804 .expect("trace task should run on fallback runtime");
805 assert_eq!(thread_name, "rocketmq-client-trace-test");
806 handle.abort();
807 }
808
809 #[test]
810 fn test_new_dispatcher() {
811 let dispatcher = AsyncTraceDispatcher::new("TestGroup", Type::Produce, 20, "TRACE_TOPIC", None);
812
813 assert!(!dispatcher.is_started());
814 assert!(!dispatcher.is_stopped());
815 assert_eq!(dispatcher.get_discard_count(), 0);
816 }
817
818 #[test]
819 fn test_gen_group_name() {
820 let dispatcher = AsyncTraceDispatcher::new("TestGroup", Type::Produce, 20, "TRACE_TOPIC", None);
821
822 let name1 = dispatcher.gen_group_name();
823 let name2 = dispatcher.gen_group_name();
824
825 assert!(name1.starts_with(TraceConstants::GROUP_NAME_PREFIX));
826 assert!(name1.contains("TestGroup"));
827 assert_ne!(name1, name2); }
829
830 #[test]
831 fn test_append_before_start() {
832 let dispatcher = AsyncTraceDispatcher::new("TestGroup", Type::Produce, 20, "TRACE_TOPIC", None);
833
834 let ctx = TraceContext::default();
835
836 let result = dispatcher.append(&ctx);
838 assert!(result);
839 }
840
841 #[test]
842 fn test_batch_num_capping() {
843 let dispatcher = AsyncTraceDispatcher::new(
845 "TestGroup",
846 Type::Produce,
847 100, "TRACE_TOPIC",
849 None,
850 );
851
852 assert_eq!(dispatcher.config.batch_num, 20); }
854
855 #[test]
856 fn test_default_trace_topic() {
857 let dispatcher = AsyncTraceDispatcher::new(
859 "TestGroup",
860 Type::Produce,
861 20,
862 "", None,
864 );
865
866 assert_eq!(dispatcher.config.trace_topic_name, TopicValidator::RMQ_SYS_TRACE_TOPIC);
867 }
868
869 #[test]
870 fn test_config_values() {
871 let dispatcher = AsyncTraceDispatcher::new("TestGroup", Type::Consume, 15, "CUSTOM_TOPIC", None);
872
873 assert_eq!(dispatcher.config.group, "TestGroup");
874 assert_eq!(dispatcher.config.type_, Type::Consume);
875 assert_eq!(dispatcher.config.batch_num, 15);
876 assert_eq!(dispatcher.config.max_msg_size, 128_000);
877 assert_eq!(dispatcher.config.flush_interval, Duration::from_secs(5));
878 assert_eq!(dispatcher.config.trace_topic_name, "CUSTOM_TOPIC");
879 }
880
881 #[test]
882 fn test_set_host_producer() {
883 let dispatcher = AsyncTraceDispatcher::new("TestGroup", Type::Produce, 20, "TRACE_TOPIC", None);
884
885 assert!(dispatcher.state.host_producer.read().is_none());
886 }
887
888 #[test]
889 fn test_set_namespace_v2() {
890 let dispatcher = AsyncTraceDispatcher::new("TestGroup", Type::Produce, 20, "TRACE_TOPIC", None);
891
892 dispatcher.set_namespace_v2(Some(CheetahString::from_static_str("test-namespace")));
893
894 let ns = dispatcher.state.namespace_v2.read();
895 assert!(ns.is_some());
896 assert_eq!(ns.as_ref().unwrap().to_string(), "test-namespace");
897 }
898
899 #[test]
900 fn test_flush_before_start() {
901 let dispatcher = AsyncTraceDispatcher::new("TestGroup", Type::Produce, 20, "TRACE_TOPIC", None);
902
903 let result = dispatcher.flush();
905 assert!(result.is_err());
906 }
907
908 #[test]
909 fn java_shutdown_hook_aliases_are_noop_under_drop_based_shutdown() {
910 let dispatcher = AsyncTraceDispatcher::new("TestGroup", Type::Produce, 20, "TRACE_TOPIC", None);
911
912 dispatcher.register_shutdown_hook();
913 dispatcher.register_shut_down_hook();
914 dispatcher.remove_shutdown_hook();
915
916 assert!(!dispatcher.is_started());
917 assert!(!dispatcher.is_stopped());
918 }
919
920 #[tokio::test]
921 async fn test_worker_loop_basic() {
922 let (tx, rx) = mpsc::channel(10);
928 let state = Arc::new(DispatcherState::new());
929 let producer = Arc::new(tokio::sync::Mutex::new(DefaultMQProducer::builder().build()));
930 let config = TraceDispatcherConfig {
931 group: "Test".to_string(),
932 type_: Type::Produce,
933 batch_num: 5,
934 max_msg_size: 128_000,
935 flush_interval: Duration::from_millis(100),
936 trace_topic_name: "TRACE".to_string(),
937 rpc_hook: None,
938 };
939
940 drop(tx);
942
943 let result = worker_loop(rx, state, producer, config).await;
946
947 assert!(
949 result.is_err(),
950 "Expected error from producer start in test environment"
951 );
952 }
953}