Skip to main content

rocketmq_client_rust/trace/
async_trace_dispatcher.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::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// Configuration for the async trace dispatcher.
59#[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
70// Shared state for the async trace dispatcher.
71struct 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
113/// Asynchronous trace dispatcher for RocketMQ message tracing.
114///
115/// Collects trace contexts from producers and consumers, batches them according
116/// to configured thresholds, and asynchronously sends them to a trace topic.
117/// The dispatcher uses a bounded channel for non-blocking appends and spawns
118/// background tasks for batch processing.
119///
120/// Trace contexts are grouped by topic before being encoded and sent. Messages
121/// exceeding the configured size limit are split into multiple trace messages.
122///
123/// # Implementation
124///
125/// Uses a `tokio::mpsc::channel` with a capacity of 2048 for queuing trace contexts.
126/// A single worker task periodically flushes batches based on count or time thresholds.
127/// Send operations are performed concurrently via `tokio::spawn`.
128///
129/// # Discard Policy
130///
131/// When the internal queue is full, new trace contexts are discarded and the
132/// discard counter is incremented. This prevents blocking the caller.
133pub 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    /// Creates a new async trace dispatcher.
146    ///
147    /// The dispatcher is created in a stopped state and must be started via
148    /// [`start`](Self::start) before it can process trace contexts.
149    ///
150    /// # Arguments
151    ///
152    /// * `group` - Producer or consumer group name used for trace producer identification
153    /// * `type_` - Trace type, either `Type::Produce` or `Type::Consume`
154    /// * `batch_num` - Maximum number of contexts per batch (capped at 20)
155    /// * `trace_topic_name` - Destination topic for trace messages (uses system default if empty)
156    /// * `rpc_hook` - Optional RPC hook applied to the internal trace producer
157    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, // 128KB
177            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    // Generates a unique group name for the trace producer using an atomic counter.
195    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    /// Returns the number of trace contexts discarded due to queue overflow.
207    pub fn get_discard_count(&self) -> u64 {
208        self.state.discard_count.load(Ordering::Relaxed)
209    }
210
211    /// Returns the approximate queue depth.
212    ///
213    /// # Note
214    ///
215    /// The tokio mpsc channel does not expose queue size. This method returns 0.
216    pub fn queue_size(&self) -> usize {
217        // mpsc::Sender doesn't expose queue size directly
218        // This is an approximation
219        0
220    }
221
222    /// Returns whether the dispatcher has been started.
223    pub fn is_started(&self) -> bool {
224        self.state.is_started.load(Ordering::SeqCst)
225    }
226
227    /// Returns whether the dispatcher has been stopped.
228    pub fn is_stopped(&self) -> bool {
229        self.state.is_stopped.load(Ordering::SeqCst)
230    }
231
232    /// Returns the configured maximum number of trace contexts per batch.
233    pub fn batch_num(&self) -> usize {
234        self.config.batch_num
235    }
236
237    /// Associates the dispatcher with a host producer implementation.
238    ///
239    /// The host producer reference is stored for potential future use in trace context enrichment.
240    pub fn set_host_producer(&self, host_producer: ArcMut<DefaultMQProducerImpl>) {
241        *self.state.host_producer.write() = Some(host_producer);
242    }
243
244    /// Associates the dispatcher with a host consumer implementation.
245    ///
246    /// The host consumer reference is stored for potential future use in trace context enrichment.
247    pub fn set_host_consumer(&self, host_consumer: ArcMut<DefaultMQPushConsumerImpl>) {
248        *self.state.host_consumer.write() = Some(host_consumer);
249    }
250
251    /// Sets the namespace v2 configuration for the dispatcher.
252    pub fn set_namespace_v2(&self, namespace_v2: Option<CheetahString>) {
253        *self.state.namespace_v2.write() = namespace_v2;
254    }
255
256    /// Sets whether the internal trace producer should use TLS.
257    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    /// Returns a reference to the internal trace producer.
266    ///
267    /// The producer is available only after the dispatcher has been started.
268    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    /// Starts the trace dispatcher and its background worker task.
287    ///
288    /// Initializes the internal trace producer, spawns the worker task for
289    /// batch processing, and marks the dispatcher as started. This method
290    /// can be called only once; subsequent calls are ignored.
291    ///
292    /// # Errors
293    ///
294    /// Returns an error if the dispatcher has already been started or if
295    /// internal state initialization fails.
296    fn start(&self, name_srv_addr: &str, access_channel: AccessChannel) -> RocketMQResult<()> {
297        // CAS to ensure we only start once
298        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        // Initialize TraceProducer (without starting it)
311        let producer_group = self.gen_group_name();
312
313        // Create client config with proper trace settings
314        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); // Prevents recursive trace operations
317        client_config.set_vip_channel_enabled(false); // Disable VIP channel (matches Java implementation)
318        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        // Save producer and access channel
335        *self.trace_producer.write() = Some(Arc::new(tokio::sync::Mutex::new(producer)));
336        *self.state.access_channel.write() = access_channel;
337
338        // Take the receiver
339        let rx = self
340            .rx
341            .lock()
342            .take()
343            .ok_or_else(|| RocketMQError::not_initialized("Dispatcher already started"))?;
344
345        // Get producer reference for worker
346        let producer = self
347            .trace_producer
348            .read()
349            .clone()
350            .ok_or_else(|| RocketMQError::not_initialized("Producer not initialized"))?;
351
352        // Spawn worker task
353        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    /// Appends a trace context to the dispatcher queue.
375    ///
376    /// This method does not block the calling thread. If the internal queue is full,
377    /// the trace context is discarded and the discard counter is incremented.
378    ///
379    /// # Arguments
380    ///
381    /// * `ctx` - Trace context, must be a `TraceContext` instance
382    ///
383    /// # Returns
384    ///
385    /// Returns `true` if the context was successfully queued, `false` if it was
386    /// discarded due to queue overflow or type mismatch.
387    fn append(&self, ctx: &dyn Any) -> bool {
388        // Downcast to TraceContext
389        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        // Try to send without blocking
398        match self.tx.try_send(trace_ctx.clone()) {
399            Ok(_) => true,
400            Err(mpsc::error::TrySendError::Full(_)) => {
401                // Queue full, discard and increment counter
402                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    /// Flushes pending trace contexts by allowing the worker task to process them.
417    ///
418    /// Blocks the calling thread briefly to give the worker task time to process
419    /// pending contexts. This is a best-effort operation and does not guarantee
420    /// all contexts are sent.
421    ///
422    /// # Errors
423    ///
424    /// Returns an error if the dispatcher has not been started.
425    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        // Allows the worker task time to process queued items
433        std::thread::sleep(Duration::from_millis(100));
434
435        info!("Flush completed");
436        Ok(())
437    }
438
439    /// Shuts down the dispatcher and stops the background worker task.
440    ///
441    /// Attempts to flush remaining trace contexts before stopping. This method
442    /// blocks briefly to allow pending data to be processed. The dispatcher
443    /// cannot be restarted after shutdown.
444    fn shutdown(&self) {
445        if !self.state.is_started.load(Ordering::SeqCst) {
446            warn!("AsyncTraceDispatcher not started");
447            return;
448        }
449
450        // Set stopped flag
451        self.state.is_stopped.store(true, Ordering::SeqCst);
452
453        info!("Shutting down AsyncTraceDispatcher...");
454
455        // Flush remaining data
456        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        // Clear state
469        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            // Best effort flush before drop
501            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
531// Main worker loop that processes trace contexts in batches.
532// Receives contexts from the channel and flushes when batch size or time threshold is reached.
533async 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    // Start the producer in the async worker context
540    {
541        let mut producer_guard = producer.lock().await;
542        let start_result = producer_guard.start().await;
543        drop(producer_guard); // Explicitly drop before checking result
544
545        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            // Timer tick every 5ms
561            _ = 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                // Check if we should stop
573                if state.is_stopped.load(Ordering::SeqCst) && buffer.is_empty() {
574                    info!("Worker loop exiting (stopped flag set)");
575                    break;
576                }
577            }
578
579            // Receive trace context or detect channel close
580            result = rx.recv() => {
581                match result {
582                    Some(ctx) => {
583                        buffer.push(ctx);
584
585                        // Flush immediately if batch is full
586                        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                        // Channel closed
593                        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
607// Flushes the buffer by awaiting the trace send in the worker task.
608async 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    // Take ownership of buffer contents (zero-copy swap)
619    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
626// Processes and sends trace data in batches.
627// Groups contexts by topic, encodes them, and delegates to flush_data.
628async 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    // Group trace contexts by (topic, trace_topic)
635    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        // Determine trace topic based on access channel
653        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        // Encode trace context
664        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    // Send each group
670    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
691// Aggregates trace transfer beans and sends them as messages.
692// Splits large payloads into multiple messages based on max_msg_size.
693async 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        // Collect keys
710        key_set.extend(bean.trans_key().iter().cloned());
711
712        // Append data
713        buffer.push_str(bean.trans_data());
714
715        // Send if buffer is full
716        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    // Send remaining data
724    if !buffer.is_empty() {
725        send_trace_message(&key_set, &buffer, trace_topic, producer, state).await?;
726    }
727
728    Ok(())
729}
730
731// Sends a single trace message to the broker.
732// Constructs the message with keys and body, uses MessageQueue selector for
733// round-robin queue selection, matching Java's behavior.
734async 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    // Use selector-based send for queue selection (matching Java)
757    // Selector implements round-robin across available queues
758    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        // Round-robin queue selection
764        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    // The worker already runs off the application send path, so wait for the
770    // trace send here to make shutdown/flush deterministic.
771    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); // Different counter values
828    }
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        // Should succeed even before start (queue is available)
837        let result = dispatcher.append(&ctx);
838        assert!(result);
839    }
840
841    #[test]
842    fn test_batch_num_capping() {
843        // Test that batch_num is capped at 20
844        let dispatcher = AsyncTraceDispatcher::new(
845            "TestGroup",
846            Type::Produce,
847            100, // Request 100
848            "TRACE_TOPIC",
849            None,
850        );
851
852        assert_eq!(dispatcher.config.batch_num, 20); // Capped at 20
853    }
854
855    #[test]
856    fn test_default_trace_topic() {
857        // Test default topic when empty string provided
858        let dispatcher = AsyncTraceDispatcher::new(
859            "TestGroup",
860            Type::Produce,
861            20,
862            "", // Empty topic
863            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        // Should return error since not started
904        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        // This test verifies the worker loop structure without actually starting
923        // the producer (which requires a real broker connection).
924        // The worker_loop will return an error because producer.start() fails,
925        // but that's expected in a unit test environment.
926
927        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        // Close the channel immediately
941        drop(tx);
942
943        // Worker will try to start producer and fail (expected in test environment)
944        // The test just verifies no panic occurs
945        let result = worker_loop(rx, state, producer, config).await;
946
947        // In unit test environment, start() will fail due to missing broker
948        assert!(
949            result.is_err(),
950            "Expected error from producer start in test environment"
951        );
952    }
953}