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::mpsc as std_mpsc;
24use std::sync::Arc;
25use std::time::Duration;
26use std::time::Instant;
27use std::time::SystemTime;
28use std::time::UNIX_EPOCH;
29
30use crate::ClientError;
31use crate::ClientResult;
32use bytes::Bytes;
33use cheetah_string::CheetahString;
34use parking_lot::Mutex;
35use parking_lot::RwLock;
36use rocketmq_model::common::message::message_queue::MessageQueue;
37use rocketmq_model::common::message::message_single::Message;
38use rocketmq_model::common::topic::TopicValidator;
39use rocketmq_runtime::ChildServiceContext;
40use rocketmq_runtime::ShutdownReport;
41use rocketmq_transport::api::RPCHook;
42use serde::Serialize;
43use tokio::sync::mpsc;
44use tokio::sync::oneshot;
45use tokio::sync::OnceCell;
46use tracing::debug;
47use tracing::error;
48use tracing::info;
49use tracing::warn;
50
51use crate::base::access_channel::AccessChannel;
52use crate::base::client_config::ClientConfig;
53use crate::producer::default_mq_producer::DefaultMQProducer;
54use crate::runtime::spawn_client_tracked_task_with_context;
55use crate::runtime::ClientRuntime;
56use crate::runtime::ClientTrackedTaskHandle;
57use crate::trace::trace_constants::TraceConstants;
58use crate::trace::trace_context::TraceContext;
59use crate::trace::trace_data_encoder::TraceDataEncoder;
60use crate::trace::trace_dispatcher::TraceDispatcher;
61use crate::trace::trace_dispatcher::Type;
62use crate::trace::trace_transfer_bean::TraceTransferBean;
63
64const TRACE_WORKER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
66const TRACE_WORKER_FLUSH_TIMEOUT: Duration = Duration::from_secs(5);
67
68#[derive(Clone)]
69struct TraceDispatcherConfig {
70 group: String,
71 type_: Type,
72 batch_num: usize,
73 max_msg_size: usize,
74 flush_interval: Duration,
75 trace_topic_name: String,
76 rpc_hook: Option<Arc<dyn RPCHook>>,
77}
78
79struct DispatcherState {
81 is_started: AtomicBool,
82 is_stopped: AtomicBool,
83 queued_count: AtomicUsize,
84 discard_count: AtomicU64,
85 last_flush_time: AtomicU64,
86 send_which_queue: AtomicUsize,
87 access_channel: RwLock<AccessChannel>,
88 client_host: RwLock<Option<CheetahString>>,
89 namespace_v2: RwLock<Option<CheetahString>>,
90}
91
92impl DispatcherState {
93 fn new() -> Self {
94 Self {
95 is_started: AtomicBool::new(false),
96 is_stopped: AtomicBool::new(false),
97 queued_count: AtomicUsize::new(0),
98 discard_count: AtomicU64::new(0),
99 last_flush_time: AtomicU64::new(0),
100 send_which_queue: AtomicUsize::new(0),
101 access_channel: RwLock::new(AccessChannel::Local),
102 client_host: RwLock::new(None),
103 namespace_v2: RwLock::new(None),
104 }
105 }
106}
107
108enum TraceFlushResponder {
109 Blocking(std_mpsc::Sender<ClientResult<()>>),
110 Async(oneshot::Sender<ClientResult<()>>),
111}
112
113impl TraceFlushResponder {
114 fn send(self, result: ClientResult<()>) {
115 match self {
116 Self::Blocking(sender) => {
117 let _ = sender.send(result);
118 }
119 Self::Async(sender) => {
120 let _ = sender.send(result);
121 }
122 }
123 }
124}
125
126enum TraceWorkerCommand {
127 Trace(TraceContext),
128 Flush(TraceFlushResponder),
129 Shutdown,
130}
131
132enum TraceTaskHandle {
133 Tracked(ClientTrackedTaskHandle),
134}
135
136impl TraceTaskHandle {
137 fn abort(self) {
138 match self {
139 Self::Tracked(handle) => {
140 let report = handle.shutdown_now();
141 if !report.is_healthy() {
142 warn!(report = %report.to_json(), "trace worker shutdown_now report is unhealthy");
143 }
144 }
145 }
146 }
147
148 async fn shutdown_async(self, timeout: Duration) -> bool {
149 let report = self.shutdown_report_async(timeout).await;
150 if !report.is_healthy() {
151 warn!(report = %report.to_json(), "trace worker shutdown report is unhealthy");
152 }
153 report.is_healthy()
154 }
155
156 fn shutdown(self, timeout: Duration) -> bool {
157 let (report, completed) = self.shutdown_report(timeout);
158 if !completed || !report.is_healthy() {
159 warn!(
160 completed_before_timeout = completed,
161 report = %report.to_json(),
162 "trace worker blocking shutdown report is unhealthy"
163 );
164 }
165 completed && report.is_healthy()
166 }
167
168 async fn shutdown_report_async(self, timeout: Duration) -> ShutdownReport {
169 match self {
170 Self::Tracked(handle) => handle.shutdown(timeout).await,
171 }
172 }
173
174 fn shutdown_report(self, timeout: Duration) -> (ShutdownReport, bool) {
175 match self {
176 Self::Tracked(handle) => handle.shutdown_blocking(timeout),
177 }
178 }
179
180 fn task_count(&self) -> usize {
181 match self {
182 Self::Tracked(handle) => handle.task_count(),
183 }
184 }
185}
186
187#[derive(Debug, Clone, Serialize)]
188pub struct TraceWorkerLifecycleProbe {
189 pub task_count_before_shutdown: usize,
190 pub remaining_tasks_after_shutdown: usize,
191 pub completed: usize,
192 pub cancelled: usize,
193 pub aborted: usize,
194 pub timed_out: usize,
195 pub shutdown_elapsed_us: u128,
196 pub healthy: bool,
197}
198
199pub struct AsyncTraceDispatcher {
221 client_runtime: Arc<ClientRuntime>,
222 service_context: ChildServiceContext,
223 config: TraceDispatcherConfig,
224 state: Arc<DispatcherState>,
225 use_tls: AtomicBool,
226 tx: mpsc::Sender<TraceWorkerCommand>,
227 rx: Mutex<Option<mpsc::Receiver<TraceWorkerCommand>>>,
228 worker_handle: Mutex<Option<TraceTaskHandle>>,
229 shutdown_completion: OnceCell<()>,
230 trace_producer: RwLock<Option<Arc<tokio::sync::Mutex<DefaultMQProducer>>>>,
231 instance_counter: Arc<AtomicUsize>,
232}
233
234impl AsyncTraceDispatcher {
235 pub fn new(
248 client_runtime: Arc<ClientRuntime>,
249 group: &str,
250 type_: Type,
251 batch_num: usize,
252 trace_topic_name: &str,
253 rpc_hook: Option<Arc<dyn RPCHook>>,
254 ) -> Self {
255 let (tx, rx) = mpsc::channel(2048);
256
257 let trace_topic = if trace_topic_name.is_empty() {
258 TopicValidator::RMQ_SYS_TRACE_TOPIC.to_string()
259 } else {
260 trace_topic_name.to_string()
261 };
262
263 let config = TraceDispatcherConfig {
264 group: group.to_string(),
265 type_,
266 batch_num: batch_num.min(20),
267 max_msg_size: 128_000, flush_interval: Duration::from_secs(5),
269 trace_topic_name: trace_topic,
270 rpc_hook,
271 };
272
273 Self {
274 service_context: client_runtime.component("trace-dispatcher"),
275 client_runtime,
276 config,
277 state: Arc::new(DispatcherState::new()),
278 use_tls: AtomicBool::new(false),
279 tx,
280 rx: Mutex::new(Some(rx)),
281 worker_handle: Mutex::new(None),
282 shutdown_completion: OnceCell::new(),
283 trace_producer: RwLock::new(None),
284 instance_counter: Arc::new(AtomicUsize::new(0)),
285 }
286 }
287
288 fn gen_group_name(&self) -> String {
290 let counter = self.instance_counter.fetch_add(1, Ordering::SeqCst);
291 format!(
292 "{}-{}-{:?}-{}",
293 TraceConstants::GROUP_NAME_PREFIX,
294 self.config.group,
295 self.config.type_,
296 counter
297 )
298 }
299
300 pub fn get_discard_count(&self) -> u64 {
302 self.state.discard_count.load(Ordering::Relaxed)
303 }
304
305 pub fn queue_size(&self) -> usize {
307 self.state.queued_count.load(Ordering::Acquire)
308 }
309
310 pub fn is_started(&self) -> bool {
312 self.state.is_started.load(Ordering::SeqCst)
313 }
314
315 pub fn is_stopped(&self) -> bool {
317 self.state.is_stopped.load(Ordering::SeqCst)
318 }
319
320 pub fn batch_num(&self) -> usize {
322 self.config.batch_num
323 }
324
325 pub fn set_client_host(&self, client_host: Option<CheetahString>) {
327 *self.state.client_host.write() = client_host;
328 }
329
330 pub fn set_namespace_v2(&self, namespace_v2: Option<CheetahString>) {
332 *self.state.namespace_v2.write() = namespace_v2;
333 }
334
335 pub fn set_use_tls(&self, use_tls: bool) {
337 self.use_tls.store(use_tls, Ordering::Release);
338 }
339
340 pub fn is_use_tls(&self) -> bool {
341 self.use_tls.load(Ordering::Acquire)
342 }
343
344 pub fn get_trace_producer(&self) -> Option<Arc<tokio::sync::Mutex<DefaultMQProducer>>> {
348 self.trace_producer.read().clone()
349 }
350
351 pub fn trace_topic_name(&self) -> &str {
352 self.config.trace_topic_name.as_str()
353 }
354
355 pub async fn flush_async(&self) -> ClientResult<()> {
357 if !self.state.is_started.load(Ordering::SeqCst) {
358 return Err(ClientError::not_initialized("Dispatcher not started"));
359 }
360
361 info!("Flushing trace data...");
362 let (sender, receiver) = oneshot::channel();
363 self.tx
364 .send(TraceWorkerCommand::Flush(TraceFlushResponder::Async(sender)))
365 .await
366 .map_err(|_| trace_dispatcher_interrupted("worker is stopped"))?;
367
368 match tokio::time::timeout(TRACE_WORKER_FLUSH_TIMEOUT, receiver).await {
369 Ok(Ok(result)) => result?,
370 Ok(Err(_)) => {
371 return Err(trace_dispatcher_interrupted("worker dropped flush acknowledgement"));
372 }
373 Err(_) => {
374 return Err(trace_dispatcher_flush_timeout());
375 }
376 }
377 info!("Flush completed");
378 Ok(())
379 }
380
381 pub async fn shutdown_async(&self) {
386 if !self.state.is_started.load(Ordering::SeqCst) {
387 if self.shutdown_completion.get().is_none() {
388 warn!("AsyncTraceDispatcher not started");
389 }
390 return;
391 }
392
393 self.shutdown_completion
394 .get_or_init(|| self.shutdown_async_once())
395 .await;
396 self.state.is_started.store(false, Ordering::SeqCst);
397 }
398
399 async fn shutdown_async_once(&self) {
400 info!("Shutting down AsyncTraceDispatcher...");
401
402 if let Err(error) = self.flush_async().await {
403 error!("Flush failed during shutdown: {:?}", error);
404 }
405
406 self.request_worker_shutdown();
407
408 let handle = { self.worker_handle.lock().take() };
409 if let Some(handle) = handle {
410 let stopped = handle.shutdown_async(TRACE_WORKER_SHUTDOWN_TIMEOUT).await;
411 if !stopped {
412 warn!(
413 timeout_ms = TRACE_WORKER_SHUTDOWN_TIMEOUT.as_millis(),
414 "Trace worker did not stop before timeout and was aborted"
415 );
416 }
417 }
418
419 if self.trace_producer.read().is_some() {
420 info!("Trace producer shut down");
421 }
422
423 *self.trace_producer.write() = None;
424
425 info!("AsyncTraceDispatcher stopped");
426 }
427
428 pub fn register_shutdown_hook(&self) {}
429
430 pub fn register_shut_down_hook(&self) {
431 self.register_shutdown_hook();
432 }
433
434 pub fn remove_shutdown_hook(&self) {}
435
436 fn request_worker_shutdown(&self) {
437 self.state.is_stopped.store(true, Ordering::SeqCst);
438 if let Err(error) = self.tx.try_send(TraceWorkerCommand::Shutdown) {
439 debug!(%error, "trace worker shutdown command was not queued");
440 }
441 }
442}
443
444impl TraceDispatcher for AsyncTraceDispatcher {
445 fn start(&self, name_srv_addr: &str, access_channel: AccessChannel) -> ClientResult<()> {
456 if self
458 .state
459 .is_started
460 .compare_exchange(false, true, Ordering::SeqCst, Ordering::Relaxed)
461 .is_err()
462 {
463 warn!("AsyncTraceDispatcher already started");
464 return Ok(());
465 }
466
467 info!("Starting AsyncTraceDispatcher...");
468
469 let producer_group = self.gen_group_name();
471
472 let mut client_config = ClientConfig::default();
474 client_config.set_namesrv_addr(CheetahString::from_slice(name_srv_addr));
475 client_config.set_enable_trace(false); client_config.set_vip_channel_enabled(false); client_config.set_use_tls(self.use_tls.load(Ordering::Acquire));
478
479 let mut builder = DefaultMQProducer::builder(Arc::clone(&self.client_runtime))
480 .producer_group(&producer_group)
481 .client_config(client_config)
482 .send_msg_timeout(5000)
483 .max_message_size(self.config.max_msg_size as u32);
484
485 if let Some(rpc_hook) = self.config.rpc_hook.clone() {
486 builder = builder.rpc_hook(rpc_hook);
487 }
488
489 let producer = builder.build();
490
491 info!("Trace producer initialized: group={}", producer_group);
492
493 *self.trace_producer.write() = Some(Arc::new(tokio::sync::Mutex::new(producer)));
495 *self.state.access_channel.write() = access_channel;
496
497 let rx = self
499 .rx
500 .lock()
501 .take()
502 .ok_or_else(|| ClientError::not_initialized("Dispatcher already started"))?;
503
504 let producer = self
506 .trace_producer
507 .read()
508 .clone()
509 .ok_or_else(|| ClientError::not_initialized("Producer not initialized"))?;
510
511 let state = self.state.clone();
513 let config = self.config.clone();
514
515 let handle = match spawn_trace_task(&self.service_context, "rocketmq-client-trace-worker", async move {
516 if let Err(e) = worker_loop(rx, state, producer, config).await {
517 error!("Worker loop failed: {:?}", e);
518 }
519 }) {
520 Ok(handle) => handle,
521 Err(error) => {
522 self.state.is_started.store(false, Ordering::SeqCst);
523 return Err(error);
524 }
525 };
526
527 *self.worker_handle.lock() = Some(handle);
528
529 info!("AsyncTraceDispatcher started successfully");
530 Ok(())
531 }
532
533 fn append(&self, ctx: &dyn Any) -> bool {
547 let trace_ctx = match ctx.downcast_ref::<TraceContext>() {
549 Some(ctx) => ctx,
550 None => {
551 warn!("Failed to downcast to TraceContext");
552 return false;
553 }
554 };
555
556 self.state.queued_count.fetch_add(1, Ordering::AcqRel);
558 match self.tx.try_send(TraceWorkerCommand::Trace(trace_ctx.clone())) {
559 Ok(_) => true,
560 Err(mpsc::error::TrySendError::Full(_)) => {
561 decrement_queued_trace_count(&self.state);
562 let count = self.state.discard_count.fetch_add(1, Ordering::Relaxed);
564 warn!(
565 "Trace context queue full (discard count: {}), data discarded",
566 count + 1
567 );
568 false
569 }
570 Err(mpsc::error::TrySendError::Closed(_)) => {
571 decrement_queued_trace_count(&self.state);
572 error!("Trace dispatcher channel closed");
573 false
574 }
575 }
576 }
577
578 fn flush(&self) -> ClientResult<()> {
587 if !self.state.is_started.load(Ordering::SeqCst) {
588 return Err(ClientError::not_initialized("Dispatcher not started"));
589 }
590
591 info!("Flushing trace data...");
592
593 let (sender, receiver) = std_mpsc::channel();
594 self.tx
595 .try_send(TraceWorkerCommand::Flush(TraceFlushResponder::Blocking(sender)))
596 .map_err(trace_dispatcher_interrupted_source)?;
597
598 match receiver.recv_timeout(TRACE_WORKER_FLUSH_TIMEOUT) {
599 Ok(result) => result?,
600 Err(std_mpsc::RecvTimeoutError::Timeout) => {
601 return Err(trace_dispatcher_flush_timeout());
602 }
603 Err(std_mpsc::RecvTimeoutError::Disconnected) => {
604 return Err(trace_dispatcher_interrupted("worker dropped flush acknowledgement"));
605 }
606 }
607
608 info!("Flush completed");
609 Ok(())
610 }
611
612 fn shutdown(&self) {
618 if !self.state.is_started.load(Ordering::SeqCst) {
619 warn!("AsyncTraceDispatcher not started");
620 return;
621 }
622
623 info!("Shutting down AsyncTraceDispatcher...");
624
625 if let Err(e) = self.flush() {
627 error!("Flush failed during shutdown: {:?}", e);
628 }
629
630 self.request_worker_shutdown();
631
632 if let Some(handle) = self.worker_handle.lock().take() {
633 let stopped = handle.shutdown(TRACE_WORKER_SHUTDOWN_TIMEOUT);
634 if !stopped {
635 warn!(
636 timeout_ms = TRACE_WORKER_SHUTDOWN_TIMEOUT.as_millis(),
637 "Trace worker did not stop before timeout and was aborted"
638 );
639 }
640 }
641
642 if let Some(_producer) = self.trace_producer.read().as_ref() {
643 info!("Trace producer shut down");
644 }
645
646 self.state.is_started.store(false, Ordering::SeqCst);
648 *self.trace_producer.write() = None;
649
650 info!("AsyncTraceDispatcher stopped");
651 }
652
653 fn as_any(&self) -> &dyn Any {
654 self
655 }
656
657 fn as_mut_any(&mut self) -> &mut dyn Any {
658 self
659 }
660
661 fn trace_topic_name(&self) -> Option<&str> {
662 Some(self.config.trace_topic_name.as_str())
663 }
664
665 fn trace_client_host(&self) -> Option<CheetahString> {
666 self.state.client_host.read().clone()
667 }
668}
669
670impl Drop for AsyncTraceDispatcher {
671 fn drop(&mut self) {
672 if self.state.is_started.load(Ordering::SeqCst) {
673 warn!("AsyncTraceDispatcher dropped without completed explicit shutdown; pending trace data may be lost");
674 self.request_worker_shutdown();
675 if let Some(handle) = self.worker_handle.lock().take() {
676 handle.abort();
677 }
678 self.state.is_started.store(false, Ordering::SeqCst);
679 }
680 }
681}
682
683fn spawn_trace_task<F>(
684 service_context: &ChildServiceContext,
685 thread_name: &'static str,
686 task: F,
687) -> ClientResult<TraceTaskHandle>
688where
689 F: Future<Output = ()> + Send + 'static,
690{
691 spawn_client_tracked_task_with_context(service_context, thread_name, Box::pin(task))
692 .map(TraceTaskHandle::Tracked)
693 .map_err(|error| trace_dispatcher_startup_failed(thread_name, error))
694}
695
696fn trace_dispatcher_interrupted(_reason: impl Into<String>) -> ClientError {
697 ClientError::service_failed("trace_dispatcher_interrupted")
698}
699
700fn trace_dispatcher_interrupted_source(source: impl std::error::Error + Send + Sync + 'static) -> ClientError {
701 ClientError::service_source("trace_dispatcher_interrupted", source)
702}
703
704fn trace_dispatcher_flush_timeout() -> ClientError {
705 ClientError::timeout("trace_dispatcher_flush", TRACE_WORKER_FLUSH_TIMEOUT.as_millis() as u64)
706}
707
708fn trace_dispatcher_startup_failed(
709 thread_name: &'static str,
710 error: impl std::error::Error + Send + Sync + 'static,
711) -> ClientError {
712 ClientError::service_source(thread_name, error)
713}
714
715fn decrement_queued_trace_count(state: &DispatcherState) {
716 let _ = state
717 .queued_count
718 .try_update(Ordering::AcqRel, Ordering::Acquire, |count| count.checked_sub(1));
719}
720
721fn reset_queued_trace_count(state: &DispatcherState) {
722 state.queued_count.store(0, Ordering::Release);
723}
724
725async fn worker_loop(
728 mut rx: mpsc::Receiver<TraceWorkerCommand>,
729 state: Arc<DispatcherState>,
730 producer: Arc<tokio::sync::Mutex<DefaultMQProducer>>,
731 config: TraceDispatcherConfig,
732) -> ClientResult<()> {
733 {
735 let mut producer_guard = producer.lock().await;
736 let start_result = producer_guard.start().await;
737 drop(producer_guard); if let Err(e) = start_result {
740 error!("Failed to start trace producer: {:?}", e);
741 reset_queued_trace_count(&state);
742 return Err(e);
743 }
744 info!("Trace producer started successfully in worker");
745 }
746
747 let mut buffer = Vec::with_capacity(config.batch_num);
748 let mut next_flush_deadline: Option<Instant> = None;
749
750 loop {
751 if state.is_stopped.load(Ordering::SeqCst) {
752 if !buffer.is_empty() {
753 if let Err(error) = flush_buffer(&mut buffer, &state, &producer, &config).await {
754 error!("flush_buffer failed before worker stop: {:?}", error);
755 }
756 }
757 reset_queued_trace_count(&state);
758 info!("Worker loop exiting (stopped flag set)");
759 break;
760 }
761
762 if buffer.is_empty() {
763 match rx.recv().await {
764 Some(TraceWorkerCommand::Trace(ctx)) => {
765 decrement_queued_trace_count(&state);
766 next_flush_deadline = Some(Instant::now() + config.flush_interval);
767 buffer.push(ctx);
768
769 if buffer.len() >= config.batch_num {
770 if let Err(error) = flush_buffer(&mut buffer, &state, &producer, &config).await {
771 error!("flush_buffer failed: {:?}", error);
772 }
773 next_flush_deadline = None;
774 }
775 }
776 Some(TraceWorkerCommand::Flush(responder)) => {
777 let result = flush_buffer(&mut buffer, &state, &producer, &config).await;
778 next_flush_deadline = None;
779 responder.send(result);
780 }
781 Some(TraceWorkerCommand::Shutdown) => {
782 state.is_stopped.store(true, Ordering::SeqCst);
783 }
784 None => {
785 info!("Worker loop exiting (channel closed)");
786 reset_queued_trace_count(&state);
787 break;
788 }
789 }
790 continue;
791 }
792
793 let flush_delay = next_flush_deadline
794 .map(|deadline| deadline.saturating_duration_since(Instant::now()))
795 .unwrap_or(config.flush_interval);
796
797 tokio::select! {
798 _ = tokio::time::sleep(flush_delay) => {
799 if !buffer.is_empty() {
800 if let Err(error) = flush_buffer(&mut buffer, &state, &producer, &config).await {
801 error!("flush_buffer failed: {:?}", error);
802 }
803 next_flush_deadline = None;
804 }
805 }
806 result = rx.recv() => {
807 match result {
808 Some(TraceWorkerCommand::Trace(ctx)) => {
809 decrement_queued_trace_count(&state);
810 buffer.push(ctx);
811
812 if buffer.len() >= config.batch_num {
813 if let Err(error) = flush_buffer(&mut buffer, &state, &producer, &config).await {
814 error!("flush_buffer failed: {:?}", error);
815 }
816 next_flush_deadline = None;
817 }
818 }
819 Some(TraceWorkerCommand::Flush(responder)) => {
820 let result = flush_buffer(&mut buffer, &state, &producer, &config).await;
821 next_flush_deadline = None;
822 responder.send(result);
823 }
824 Some(TraceWorkerCommand::Shutdown) => {
825 state.is_stopped.store(true, Ordering::SeqCst);
826 }
827 None => {
828 info!("Worker loop exiting (channel closed)");
829 if !buffer.is_empty() {
830 if let Err(error) = flush_buffer(&mut buffer, &state, &producer, &config).await {
831 error!("flush_buffer failed before worker exit: {:?}", error);
832 }
833 }
834 reset_queued_trace_count(&state);
835 break;
836 }
837 }
838 }
839 }
840 }
841
842 Ok(())
843}
844
845async fn flush_buffer(
847 buffer: &mut Vec<TraceContext>,
848 state: &Arc<DispatcherState>,
849 producer: &Arc<tokio::sync::Mutex<DefaultMQProducer>>,
850 config: &TraceDispatcherConfig,
851) -> ClientResult<()> {
852 if buffer.is_empty() {
853 record_flush_completion(state);
854 return Ok(());
855 }
856
857 let contexts = std::mem::take(buffer);
859
860 send_trace_data(contexts, state, producer, config).await?;
861 record_flush_completion(state);
862 Ok(())
863}
864
865fn record_flush_completion(state: &DispatcherState) {
866 let timestamp_ms = SystemTime::now()
867 .duration_since(UNIX_EPOCH)
868 .map(|duration| duration.as_millis().min(u128::from(u64::MAX)) as u64)
869 .unwrap_or_default();
870 state.last_flush_time.store(timestamp_ms, Ordering::Release);
871}
872
873async fn send_trace_data(
876 contexts: Vec<TraceContext>,
877 state: &Arc<DispatcherState>,
878 producer: &Arc<tokio::sync::Mutex<DefaultMQProducer>>,
879 config: &TraceDispatcherConfig,
880) -> ClientResult<()> {
881 let mut trans_bean_map: HashMap<String, Vec<TraceTransferBean>> = HashMap::with_capacity(16);
883
884 let default_access_channel = *state.access_channel.read();
885
886 for ctx in contexts {
887 let access_channel = ctx.access_channel.unwrap_or(default_access_channel);
888
889 let region_id = &ctx.region_id;
890 if region_id.is_empty() {
891 continue;
892 }
893
894 let trace_beans = match &ctx.trace_beans {
895 Some(beans) if !beans.is_empty() => beans,
896 _ => continue,
897 };
898
899 let trace_topic = match access_channel {
901 AccessChannel::Cloud => {
902 format!("{}{}", TraceConstants::TRACE_TOPIC_PREFIX, region_id)
903 }
904 _ => config.trace_topic_name.clone(),
905 };
906
907 let topic = &trace_beans[0].topic;
908 let key = format!("{}{}{}", topic, TraceConstants::CONTENT_SPLITOR, trace_topic);
909
910 if let Some(transfer_bean) = TraceDataEncoder::encoder_from_context_bean(&ctx) {
912 trans_bean_map.entry(key).or_default().push(transfer_bean);
913 }
914 }
915
916 for (key, beans) in trans_bean_map {
918 let parts: Vec<&str> = key.split(TraceConstants::CONTENT_SPLITOR).collect();
919 if parts.len() != 2 {
920 warn!("Invalid trace key: {}", key);
921 continue;
922 }
923
924 let topic = parts[0];
925 let trace_topic = parts[1];
926
927 if let Err(e) = flush_data(beans, topic, trace_topic, producer, state, config).await {
928 error!(
929 "flush_data failed for topic={}, trace_topic={}: {:?}",
930 topic, trace_topic, e
931 );
932 }
933 }
934
935 Ok(())
936}
937
938async fn flush_data(
941 beans: Vec<TraceTransferBean>,
942 _topic: &str,
943 trace_topic: &str,
944 producer: &Arc<tokio::sync::Mutex<DefaultMQProducer>>,
945 state: &Arc<DispatcherState>,
946 config: &TraceDispatcherConfig,
947) -> ClientResult<()> {
948 if beans.is_empty() {
949 return Ok(());
950 }
951
952 let mut buffer = String::with_capacity(1024);
953 let mut key_set = HashSet::new();
954
955 for bean in beans {
956 key_set.extend(bean.trans_key().iter().cloned());
958
959 buffer.push_str(bean.trans_data());
961
962 if buffer.len() >= config.max_msg_size {
964 send_trace_message(&key_set, &buffer, trace_topic, producer, state).await?;
965 buffer.clear();
966 key_set.clear();
967 }
968 }
969
970 if !buffer.is_empty() {
972 send_trace_message(&key_set, &buffer, trace_topic, producer, state).await?;
973 }
974
975 Ok(())
976}
977
978async fn send_trace_message(
982 key_set: &HashSet<CheetahString>,
983 data: &str,
984 trace_topic: &str,
985 producer: &Arc<tokio::sync::Mutex<DefaultMQProducer>>,
986 state: &Arc<DispatcherState>,
987) -> ClientResult<()> {
988 let keys: Vec<String> = key_set.iter().map(|k| k.to_string()).collect();
989
990 let message = Message::builder()
991 .topic(trace_topic)
992 .body(Bytes::from(data.as_bytes().to_vec()))
993 .keys(keys)
994 .build()?;
995
996 debug!(
997 "Sending trace message: topic={}, size={}, keys={}",
998 trace_topic,
999 data.len(),
1000 key_set.len()
1001 );
1002
1003 let state_clone = state.clone();
1006 let selector = move |queues: &[MessageQueue], _msg: &Message, _arg: &()| -> Option<MessageQueue> {
1007 if queues.is_empty() {
1008 return None;
1009 }
1010 let index = state_clone.send_which_queue.fetch_add(1, Ordering::Relaxed);
1012 let pos = index % queues.len();
1013 queues.get(pos).cloned()
1014 };
1015
1016 let result = producer
1019 .lock()
1020 .await
1021 .send_with_selector_timeout(message, selector, (), 5000)
1022 .await?;
1023 debug!(
1024 "Trace message sent successfully: msgId={}, status={:?}",
1025 result
1026 .as_ref()
1027 .and_then(|send_result| send_result.msg_id.as_ref())
1028 .map_or("N/A", |id| id.as_str()),
1029 result.as_ref().map(|send_result| send_result.send_status)
1030 );
1031
1032 Ok(())
1033}
1034
1035#[doc(hidden)]
1036pub fn run_trace_queue_depth_accounting_probe(client_runtime: Arc<ClientRuntime>, queued_count: usize) -> usize {
1037 let dispatcher = AsyncTraceDispatcher::new(
1038 client_runtime,
1039 "TraceProbeGroup",
1040 Type::Produce,
1041 20,
1042 "TRACE_TOPIC",
1043 None,
1044 );
1045 let context = TraceContext::default();
1046
1047 for _ in 0..queued_count {
1048 assert!(dispatcher.append(&context));
1049 }
1050
1051 dispatcher.queue_size()
1052}
1053
1054#[doc(hidden)]
1055pub async fn run_trace_worker_lifecycle_probe(service_context: ChildServiceContext) -> TraceWorkerLifecycleProbe {
1056 let started = Arc::new(AtomicBool::new(false));
1057 let stopped = Arc::new(AtomicBool::new(false));
1058 let started_in_task = started.clone();
1059 let stopped_in_task = stopped.clone();
1060
1061 let handle = spawn_trace_task(&service_context, "rocketmq-client-trace-worker-probe", async move {
1062 started_in_task.store(true, Ordering::Release);
1063 while !stopped_in_task.load(Ordering::Acquire) {
1064 tokio::time::sleep(Duration::from_millis(1)).await;
1065 }
1066 })
1067 .expect("trace worker lifecycle probe should spawn");
1068
1069 for _ in 0..100 {
1070 if started.load(Ordering::Acquire) {
1071 break;
1072 }
1073 tokio::time::sleep(Duration::from_millis(1)).await;
1074 }
1075
1076 let task_count_before_shutdown = handle.task_count();
1077 stopped.store(true, Ordering::Release);
1078 let shutdown_started_at = Instant::now();
1079 let report = handle.shutdown_report_async(Duration::from_secs(1)).await;
1080 let shutdown_elapsed_us = shutdown_started_at.elapsed().as_micros();
1081 let remaining_tasks_after_shutdown = report.remaining_tasks.len();
1082 let healthy = started.load(Ordering::Acquire)
1083 && task_count_before_shutdown > 0
1084 && remaining_tasks_after_shutdown == 0
1085 && report.is_healthy();
1086
1087 TraceWorkerLifecycleProbe {
1088 task_count_before_shutdown,
1089 remaining_tasks_after_shutdown,
1090 completed: report.completed,
1091 cancelled: report.cancelled,
1092 aborted: report.aborted,
1093 timed_out: report.timed_out,
1094 shutdown_elapsed_us,
1095 healthy,
1096 }
1097}
1098
1099#[cfg(test)]
1100mod tests {
1101 use super::*;
1102 use crate::test_support::error_assertions::assert_context_field;
1103 use crate::test_support::error_assertions::assert_error;
1104
1105 fn test_runtime() -> Arc<ClientRuntime> {
1106 crate::runtime::test_client_runtime("async-trace-dispatcher-test")
1107 }
1108
1109 fn test_context() -> ChildServiceContext {
1110 crate::runtime::test_service_context("async-trace-dispatcher-task-test")
1111 }
1112
1113 #[test]
1114 fn trace_dispatcher_interrupted_uses_service_descriptor() {
1115 let error = trace_dispatcher_interrupted("worker is stopped");
1116
1117 assert_eq!(error.descriptor().code(), rocketmq_error::CORE_SERVICE_FAILED.code());
1118 }
1119
1120 #[test]
1121 fn trace_dispatcher_interrupted_source_preserves_cause() {
1122 let error = trace_dispatcher_interrupted_source(std::io::Error::other("flush queue closed"));
1123
1124 assert_eq!(error.descriptor().code(), rocketmq_error::CORE_SERVICE_FAILED.code());
1125 assert!(error.source_ref::<std::io::Error>().is_some());
1126 }
1127
1128 #[test]
1129 fn trace_dispatcher_flush_timeout_uses_timeout_descriptor() {
1130 let error = trace_dispatcher_flush_timeout();
1131
1132 assert_error(&error, &rocketmq_error::CORE_OPERATION_TIMED_OUT);
1133 assert_context_field(
1134 &error,
1135 "operation",
1136 rocketmq_error::ViewValueRef::Text("trace_dispatcher_flush"),
1137 );
1138 assert_context_field(
1139 &error,
1140 "timeout_ms",
1141 rocketmq_error::ViewValueRef::U64(TRACE_WORKER_FLUSH_TIMEOUT.as_millis() as u64),
1142 );
1143 }
1144
1145 #[test]
1146 fn trace_dispatcher_startup_failed_uses_service_descriptor() {
1147 let error =
1148 trace_dispatcher_startup_failed("rocketmq-client-trace-test", std::io::Error::other("task group closed"));
1149
1150 assert_eq!(error.descriptor().code(), rocketmq_error::CORE_SERVICE_FAILED.code());
1151 assert!(std::error::Error::source(&error).is_some());
1152 }
1153
1154 #[test]
1155 fn spawn_trace_task_uses_injected_runtime_context() {
1156 let (tx, rx) = std::sync::mpsc::channel();
1157
1158 let handle = spawn_trace_task(&test_context(), "rocketmq-client-trace-test", async move {
1159 tx.send(std::thread::current().name().unwrap_or_default().to_string())
1160 .expect("test receiver should be alive");
1161 })
1162 .expect("trace task should start on the injected runtime");
1163
1164 let thread_name = rx
1165 .recv_timeout(Duration::from_secs(2))
1166 .expect("trace task should run on the injected runtime");
1167 assert_eq!(thread_name, "rocketmq-client-unit-test");
1168 assert!(handle.shutdown(Duration::from_secs(1)));
1169 }
1170
1171 #[test]
1172 fn trace_task_shutdown_waits_for_completed_worker() {
1173 let handle = spawn_trace_task(&test_context(), "rocketmq-client-trace-complete-test", async move {})
1174 .expect("trace task should start on the injected runtime");
1175
1176 assert!(handle.shutdown(Duration::from_secs(1)));
1177 }
1178
1179 #[test]
1180 fn trace_task_shutdown_aborts_worker_after_timeout() {
1181 let (tx, rx) = std::sync::mpsc::channel();
1182 let handle = spawn_trace_task(&test_context(), "rocketmq-client-trace-timeout-test", async move {
1183 tx.send(()).expect("test receiver should be alive");
1184 std::future::pending::<()>().await;
1185 })
1186 .expect("trace task should start on the injected runtime");
1187
1188 rx.recv_timeout(Duration::from_secs(2))
1189 .expect("trace task should start");
1190
1191 assert!(!handle.shutdown(Duration::from_millis(20)));
1192 }
1193
1194 #[tokio::test]
1195 async fn trace_task_shutdown_async_aborts_worker_after_timeout() {
1196 struct DropFlag(Arc<AtomicBool>);
1197
1198 impl Drop for DropFlag {
1199 fn drop(&mut self) {
1200 self.0.store(true, Ordering::Release);
1201 }
1202 }
1203
1204 let started = Arc::new(AtomicBool::new(false));
1205 let dropped = Arc::new(AtomicBool::new(false));
1206 let started_in_task = started.clone();
1207 let dropped_in_task = dropped.clone();
1208 let handle = spawn_trace_task(
1209 &test_context(),
1210 "rocketmq-client-trace-timeout-async-test",
1211 async move {
1212 let _drop_flag = DropFlag(dropped_in_task);
1213 started_in_task.store(true, Ordering::Release);
1214 std::future::pending::<()>().await;
1215 },
1216 )
1217 .expect("trace task should spawn");
1218
1219 tokio::time::timeout(Duration::from_secs(1), async {
1220 while !started.load(Ordering::Acquire) {
1221 tokio::task::yield_now().await;
1222 }
1223 })
1224 .await
1225 .expect("trace task should start before shutdown");
1226
1227 assert!(!handle.shutdown_async(Duration::from_millis(20)).await);
1228 assert!(dropped.load(Ordering::Acquire));
1229 }
1230
1231 #[tokio::test]
1232 async fn concurrent_shutdown_async_flushes_and_joins_the_worker_once() {
1233 let dispatcher = Arc::new(AsyncTraceDispatcher::new(
1234 test_runtime(),
1235 "TestGroup",
1236 Type::Produce,
1237 20,
1238 "TRACE_TOPIC",
1239 None,
1240 ));
1241 let mut rx = dispatcher
1242 .rx
1243 .lock()
1244 .take()
1245 .expect("test worker receiver should be available");
1246 let flush_count = Arc::new(AtomicUsize::new(0));
1247 let worker_flush_count = Arc::clone(&flush_count);
1248 let release_first_flush = Arc::new(tokio::sync::Notify::new());
1249 let worker_release_first_flush = Arc::clone(&release_first_flush);
1250 let (first_flush_started_tx, first_flush_started_rx) = oneshot::channel();
1251
1252 let handle = spawn_trace_task(
1253 &dispatcher.service_context,
1254 "rocketmq-client-trace-idempotent-shutdown-test",
1255 async move {
1256 let mut first_flush_started_tx = Some(first_flush_started_tx);
1257 while let Some(command) = rx.recv().await {
1258 match command {
1259 TraceWorkerCommand::Trace(_) => {}
1260 TraceWorkerCommand::Flush(responder) => {
1261 let flush_index = worker_flush_count.fetch_add(1, Ordering::AcqRel);
1262 if flush_index == 0 {
1263 let sender = first_flush_started_tx
1264 .take()
1265 .expect("the first flush should signal exactly once");
1266 let _ = sender.send(());
1267 worker_release_first_flush.notified().await;
1268 }
1269 responder.send(Ok(()));
1270 }
1271 TraceWorkerCommand::Shutdown => break,
1272 }
1273 }
1274 },
1275 )
1276 .expect("test trace worker should spawn");
1277 *dispatcher.worker_handle.lock() = Some(handle);
1278 dispatcher.state.is_started.store(true, Ordering::Release);
1279
1280 let first_dispatcher = Arc::clone(&dispatcher);
1281 let first_shutdown = tokio::spawn(async move {
1282 first_dispatcher.shutdown_async().await;
1283 });
1284 first_flush_started_rx
1285 .await
1286 .expect("the first shutdown should reach the worker flush");
1287
1288 let second_shutdown = dispatcher.shutdown_async();
1289 tokio::pin!(second_shutdown);
1290 assert!(
1291 futures::poll!(second_shutdown.as_mut()).is_pending(),
1292 "the concurrent shutdown should wait for the in-flight shutdown"
1293 );
1294
1295 release_first_flush.notify_one();
1296 let (first_result, ()) = tokio::time::timeout(Duration::from_secs(1), async {
1297 tokio::join!(first_shutdown, second_shutdown)
1298 })
1299 .await
1300 .expect("both shutdown callers should complete");
1301 first_result.expect("the first shutdown task should not panic");
1302
1303 dispatcher.shutdown_async().await;
1304 assert_eq!(flush_count.load(Ordering::Acquire), 1);
1305 assert!(!dispatcher.is_started());
1306 assert!(dispatcher.is_stopped());
1307 assert_eq!(dispatcher.service_context.task_group().task_count(), 0);
1308 }
1309
1310 #[tokio::test(flavor = "current_thread")]
1311 async fn drop_requests_nonblocking_worker_cancellation() {
1312 let dispatcher = AsyncTraceDispatcher::new(test_runtime(), "TestGroup", Type::Produce, 20, "TRACE_TOPIC", None);
1313 let (worker_started_tx, worker_started_rx) = oneshot::channel();
1314 let handle = spawn_trace_task(
1315 &dispatcher.service_context,
1316 "rocketmq-client-trace-drop-test",
1317 async move {
1318 let _ = worker_started_tx.send(());
1319 std::future::pending::<()>().await;
1320 },
1321 )
1322 .expect("test trace worker should spawn");
1323 *dispatcher.worker_handle.lock() = Some(handle);
1324 dispatcher.state.is_started.store(true, Ordering::Release);
1325 let worker_group = dispatcher.service_context.task_group().clone();
1326 worker_started_rx.await.expect("test trace worker should start");
1327
1328 let (ticker_tx, ticker_rx) = oneshot::channel();
1329 tokio::spawn(async move {
1330 tokio::task::yield_now().await;
1331 let _ = ticker_tx.send(());
1332 });
1333
1334 let drop_started = Instant::now();
1335 drop(dispatcher);
1336 assert!(
1337 drop_started.elapsed() < Duration::from_millis(500),
1338 "dropping a trace dispatcher must not synchronously flush or await its worker"
1339 );
1340 tokio::time::timeout(Duration::from_secs(1), ticker_rx)
1341 .await
1342 .expect("the current-thread runtime should keep advancing after drop")
1343 .expect("the ticker task should complete");
1344 let drain_deadline = Instant::now() + Duration::from_secs(5);
1348 while worker_group.task_count() != 0 && Instant::now() < drain_deadline {
1349 tokio::task::yield_now().await;
1350 }
1351 assert_eq!(
1352 worker_group.task_count(),
1353 0,
1354 "the dropped trace worker must leave its task group"
1355 );
1356 }
1357
1358 #[tokio::test]
1359 async fn trace_worker_lifecycle_probe_reports_clean_shutdown() {
1360 let probe = run_trace_worker_lifecycle_probe(test_context()).await;
1361
1362 assert!(probe.healthy, "{probe:?}");
1363 assert_eq!(probe.remaining_tasks_after_shutdown, 0, "{probe:?}");
1364 assert_eq!(probe.aborted, 0, "{probe:?}");
1365 assert_eq!(probe.timed_out, 0, "{probe:?}");
1366 }
1367
1368 #[test]
1369 fn test_new_dispatcher() {
1370 let dispatcher = AsyncTraceDispatcher::new(test_runtime(), "TestGroup", Type::Produce, 20, "TRACE_TOPIC", None);
1371
1372 assert!(!dispatcher.is_started());
1373 assert!(!dispatcher.is_stopped());
1374 assert_eq!(dispatcher.get_discard_count(), 0);
1375 assert_eq!(dispatcher.queue_size(), 0);
1376 }
1377
1378 #[test]
1379 fn queue_size_tracks_accepted_trace_contexts() {
1380 let dispatcher = AsyncTraceDispatcher::new(test_runtime(), "TestGroup", Type::Produce, 20, "TRACE_TOPIC", None);
1381
1382 let ctx = TraceContext::default();
1383
1384 assert!(dispatcher.append(&ctx));
1385 assert_eq!(dispatcher.queue_size(), 1);
1386 }
1387
1388 #[test]
1389 fn queue_size_ignores_rejected_context_types() {
1390 let dispatcher = AsyncTraceDispatcher::new(test_runtime(), "TestGroup", Type::Produce, 20, "TRACE_TOPIC", None);
1391
1392 assert!(!dispatcher.append(&"not a trace context"));
1393 assert_eq!(dispatcher.queue_size(), 0);
1394 }
1395
1396 #[test]
1397 fn queued_trace_count_decrement_saturates_at_zero() {
1398 let state = DispatcherState::new();
1399 state.queued_count.store(1, Ordering::Release);
1400
1401 decrement_queued_trace_count(&state);
1402 assert_eq!(state.queued_count.load(Ordering::Acquire), 0);
1403
1404 decrement_queued_trace_count(&state);
1405 assert_eq!(state.queued_count.load(Ordering::Acquire), 0);
1406 }
1407
1408 #[test]
1409 fn test_gen_group_name() {
1410 let dispatcher = AsyncTraceDispatcher::new(test_runtime(), "TestGroup", Type::Produce, 20, "TRACE_TOPIC", None);
1411
1412 let name1 = dispatcher.gen_group_name();
1413 let name2 = dispatcher.gen_group_name();
1414
1415 assert!(name1.starts_with(TraceConstants::GROUP_NAME_PREFIX));
1416 assert!(name1.contains("TestGroup"));
1417 assert_ne!(name1, name2); }
1419
1420 #[test]
1421 fn test_append_before_start() {
1422 let dispatcher = AsyncTraceDispatcher::new(test_runtime(), "TestGroup", Type::Produce, 20, "TRACE_TOPIC", None);
1423
1424 let ctx = TraceContext::default();
1425
1426 let result = dispatcher.append(&ctx);
1428 assert!(result);
1429 assert_eq!(dispatcher.queue_size(), 1);
1430 }
1431
1432 #[test]
1433 fn test_batch_num_capping() {
1434 let dispatcher = AsyncTraceDispatcher::new(
1436 test_runtime(),
1437 "TestGroup",
1438 Type::Produce,
1439 100, "TRACE_TOPIC",
1441 None,
1442 );
1443
1444 assert_eq!(dispatcher.config.batch_num, 20); }
1446
1447 #[test]
1448 fn test_default_trace_topic() {
1449 let dispatcher = AsyncTraceDispatcher::new(
1451 test_runtime(),
1452 "TestGroup",
1453 Type::Produce,
1454 20,
1455 "", None,
1457 );
1458
1459 assert_eq!(dispatcher.config.trace_topic_name, TopicValidator::RMQ_SYS_TRACE_TOPIC);
1460 }
1461
1462 #[test]
1463 fn test_config_values() {
1464 let dispatcher =
1465 AsyncTraceDispatcher::new(test_runtime(), "TestGroup", Type::Consume, 15, "CUSTOM_TOPIC", None);
1466
1467 assert_eq!(dispatcher.config.group, "TestGroup");
1468 assert_eq!(dispatcher.config.type_, Type::Consume);
1469 assert_eq!(dispatcher.config.batch_num, 15);
1470 assert_eq!(dispatcher.config.max_msg_size, 128_000);
1471 assert_eq!(dispatcher.config.flush_interval, Duration::from_secs(5));
1472 assert_eq!(dispatcher.config.trace_topic_name, "CUSTOM_TOPIC");
1473 }
1474
1475 #[test]
1476 fn test_set_client_host() {
1477 let dispatcher = AsyncTraceDispatcher::new(test_runtime(), "TestGroup", Type::Produce, 20, "TRACE_TOPIC", None);
1478
1479 assert!(dispatcher.trace_client_host().is_none());
1480
1481 dispatcher.set_client_host(Some(CheetahString::from_static_str("127.0.0.1@trace-client")));
1482
1483 assert_eq!(
1484 dispatcher.trace_client_host().as_deref(),
1485 Some("127.0.0.1@trace-client")
1486 );
1487 }
1488
1489 #[test]
1490 fn test_set_namespace_v2() {
1491 let dispatcher = AsyncTraceDispatcher::new(test_runtime(), "TestGroup", Type::Produce, 20, "TRACE_TOPIC", None);
1492
1493 dispatcher.set_namespace_v2(Some(CheetahString::from_static_str("test-namespace")));
1494
1495 let ns = dispatcher.state.namespace_v2.read();
1496 assert!(ns.is_some());
1497 assert_eq!(ns.as_ref().unwrap().to_string(), "test-namespace");
1498 }
1499
1500 #[test]
1501 fn test_flush_before_start() {
1502 let dispatcher = AsyncTraceDispatcher::new(test_runtime(), "TestGroup", Type::Produce, 20, "TRACE_TOPIC", None);
1503
1504 let result = dispatcher.flush();
1506 assert!(result.is_err());
1507 }
1508
1509 #[test]
1510 fn java_shutdown_hook_aliases_are_noop_under_drop_based_shutdown() {
1511 let dispatcher = AsyncTraceDispatcher::new(test_runtime(), "TestGroup", Type::Produce, 20, "TRACE_TOPIC", None);
1512
1513 dispatcher.register_shutdown_hook();
1514 dispatcher.register_shut_down_hook();
1515 dispatcher.remove_shutdown_hook();
1516
1517 assert!(!dispatcher.is_started());
1518 assert!(!dispatcher.is_stopped());
1519 }
1520
1521 #[tokio::test]
1522 async fn flush_buffer_empty_records_completion_time() {
1523 let state = Arc::new(DispatcherState::new());
1524 let producer = Arc::new(tokio::sync::Mutex::new(
1525 DefaultMQProducer::builder(test_runtime()).build(),
1526 ));
1527 let config = TraceDispatcherConfig {
1528 group: "Test".to_string(),
1529 type_: Type::Produce,
1530 batch_num: 5,
1531 max_msg_size: 128_000,
1532 flush_interval: Duration::from_millis(100),
1533 trace_topic_name: "TRACE".to_string(),
1534 rpc_hook: None,
1535 };
1536 let mut buffer = Vec::new();
1537
1538 assert_eq!(state.last_flush_time.load(Ordering::Acquire), 0);
1539 flush_buffer(&mut buffer, &state, &producer, &config)
1540 .await
1541 .expect("empty flush should complete");
1542
1543 assert!(state.last_flush_time.load(Ordering::Acquire) > 0);
1544 }
1545
1546 #[tokio::test]
1547 async fn test_worker_loop_basic() {
1548 let (tx, rx) = mpsc::channel(10);
1554 let state = Arc::new(DispatcherState::new());
1555 let producer = Arc::new(tokio::sync::Mutex::new(
1556 DefaultMQProducer::builder(test_runtime()).build(),
1557 ));
1558 let config = TraceDispatcherConfig {
1559 group: "Test".to_string(),
1560 type_: Type::Produce,
1561 batch_num: 5,
1562 max_msg_size: 128_000,
1563 flush_interval: Duration::from_millis(100),
1564 trace_topic_name: "TRACE".to_string(),
1565 rpc_hook: None,
1566 };
1567
1568 drop(tx);
1570
1571 let result = worker_loop(rx, state, producer, config).await;
1574
1575 assert!(
1577 result.is_err(),
1578 "Expected error from producer start in test environment"
1579 );
1580 }
1581}