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::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
64// Configuration for the async trace dispatcher.
65const 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
79// Shared state for the async trace dispatcher.
80struct 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
199/// Asynchronous trace dispatcher for RocketMQ message tracing.
200///
201/// Collects trace contexts from producers and consumers, batches them according
202/// to configured thresholds, and asynchronously sends them to a trace topic.
203/// The dispatcher uses a bounded channel for non-blocking appends and spawns
204/// background tasks for batch processing.
205///
206/// Trace contexts are grouped by topic before being encoded and sent. Messages
207/// exceeding the configured size limit are split into multiple trace messages.
208///
209/// # Implementation
210///
211/// Uses a `tokio::mpsc::channel` with a capacity of 2048 for queuing trace contexts and flush
212/// commands. A single worker task periodically flushes batches based on count, time thresholds, or
213/// explicit flush requests. The worker is submitted through the client runtime helper, which uses
214/// the explicitly injected client runtime.
215///
216/// # Discard Policy
217///
218/// When the internal queue is full, new trace contexts are discarded and the
219/// discard counter is incremented. This prevents blocking the caller.
220pub 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    /// Creates a new async trace dispatcher.
236    ///
237    /// The dispatcher is created in a stopped state and must be started via
238    /// [`start`](Self::start) before it can process trace contexts.
239    ///
240    /// # Arguments
241    ///
242    /// * `group` - Producer or consumer group name used for trace producer identification
243    /// * `type_` - Trace type, either `Type::Produce` or `Type::Consume`
244    /// * `batch_num` - Maximum number of contexts per batch (capped at 20)
245    /// * `trace_topic_name` - Destination topic for trace messages (uses system default if empty)
246    /// * `rpc_hook` - Optional RPC hook applied to the internal trace producer
247    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, // 128KB
268            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    // Generates a unique group name for the trace producer using an atomic counter.
289    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    /// Returns the number of trace contexts discarded due to queue overflow.
301    pub fn get_discard_count(&self) -> u64 {
302        self.state.discard_count.load(Ordering::Relaxed)
303    }
304
305    /// Returns the approximate number of trace contexts queued for the worker.
306    pub fn queue_size(&self) -> usize {
307        self.state.queued_count.load(Ordering::Acquire)
308    }
309
310    /// Returns whether the dispatcher has been started.
311    pub fn is_started(&self) -> bool {
312        self.state.is_started.load(Ordering::SeqCst)
313    }
314
315    /// Returns whether the dispatcher has been stopped.
316    pub fn is_stopped(&self) -> bool {
317        self.state.is_stopped.load(Ordering::SeqCst)
318    }
319
320    /// Returns the configured maximum number of trace contexts per batch.
321    pub fn batch_num(&self) -> usize {
322        self.config.batch_num
323    }
324
325    /// Sets the resolved client identifier used to enrich generated trace records.
326    pub fn set_client_host(&self, client_host: Option<CheetahString>) {
327        *self.state.client_host.write() = client_host;
328    }
329
330    /// Sets the namespace v2 configuration for the dispatcher.
331    pub fn set_namespace_v2(&self, namespace_v2: Option<CheetahString>) {
332        *self.state.namespace_v2.write() = namespace_v2;
333    }
334
335    /// Sets whether the internal trace producer should use TLS.
336    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    /// Returns a reference to the internal trace producer.
345    ///
346    /// The producer is available only after the dispatcher has been started.
347    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    /// Asynchronously flushes pending trace contexts without blocking a Tokio worker thread.
356    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    /// Asynchronously shuts down the dispatcher without blocking the current Tokio worker.
382    ///
383    /// Concurrent callers await the same shutdown sequence, so pending traces
384    /// are flushed and the worker is joined at most once.
385    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    /// Starts the trace dispatcher and its background worker task.
446    ///
447    /// Initializes the internal trace producer, spawns the worker task for
448    /// batch processing, and marks the dispatcher as started. This method
449    /// can be called only once; subsequent calls are ignored.
450    ///
451    /// # Errors
452    ///
453    /// Returns an error if the dispatcher has already been started or if
454    /// internal state initialization fails.
455    fn start(&self, name_srv_addr: &str, access_channel: AccessChannel) -> ClientResult<()> {
456        // CAS to ensure we only start once
457        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        // Initialize TraceProducer (without starting it)
470        let producer_group = self.gen_group_name();
471
472        // Create client config with proper trace settings
473        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); // Prevents recursive trace operations
476        client_config.set_vip_channel_enabled(false); // Disable VIP channel (matches Java implementation)
477        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        // Save producer and access channel
494        *self.trace_producer.write() = Some(Arc::new(tokio::sync::Mutex::new(producer)));
495        *self.state.access_channel.write() = access_channel;
496
497        // Take the receiver
498        let rx = self
499            .rx
500            .lock()
501            .take()
502            .ok_or_else(|| ClientError::not_initialized("Dispatcher already started"))?;
503
504        // Get producer reference for worker
505        let producer = self
506            .trace_producer
507            .read()
508            .clone()
509            .ok_or_else(|| ClientError::not_initialized("Producer not initialized"))?;
510
511        // Spawn worker task
512        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    /// Appends a trace context to the dispatcher queue.
534    ///
535    /// This method does not block the calling thread. If the internal queue is full,
536    /// the trace context is discarded and the discard counter is incremented.
537    ///
538    /// # Arguments
539    ///
540    /// * `ctx` - Trace context, must be a `TraceContext` instance
541    ///
542    /// # Returns
543    ///
544    /// Returns `true` if the context was successfully queued, `false` if it was
545    /// discarded due to queue overflow or type mismatch.
546    fn append(&self, ctx: &dyn Any) -> bool {
547        // Downcast to TraceContext
548        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        // Try to send without blocking
557        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                // Queue full, discard and increment counter
563                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    /// Flushes pending trace contexts by requesting an acknowledgement from the worker task.
579    ///
580    /// Blocks the calling thread until the worker processes the flush command or
581    /// the flush timeout elapses.
582    ///
583    /// # Errors
584    ///
585    /// Returns an error if the dispatcher has not been started.
586    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    /// Shuts down the dispatcher and stops the background worker task.
613    ///
614    /// Attempts to flush remaining trace contexts before stopping. This method
615    /// blocks briefly to allow pending data to be processed. The dispatcher
616    /// cannot be restarted after shutdown.
617    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        // Flush remaining data
626        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        // Clear state
647        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
725// Main worker loop that processes trace contexts in batches.
726// Receives contexts from the channel and flushes when batch size or time threshold is reached.
727async 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    // Start the producer in the async worker context
734    {
735        let mut producer_guard = producer.lock().await;
736        let start_result = producer_guard.start().await;
737        drop(producer_guard); // Explicitly drop before checking result
738
739        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
845// Flushes the buffer by awaiting the trace send in the worker task.
846async 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    // Take ownership of buffer contents (zero-copy swap)
858    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
873// Processes and sends trace data in batches.
874// Groups contexts by topic, encodes them, and delegates to flush_data.
875async 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    // Group trace contexts by (topic, trace_topic)
882    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        // Determine trace topic based on access channel
900        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        // Encode trace context
911        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    // Send each group
917    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
938// Aggregates trace transfer beans and sends them as messages.
939// Splits large payloads into multiple messages based on max_msg_size.
940async 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        // Collect keys
957        key_set.extend(bean.trans_key().iter().cloned());
958
959        // Append data
960        buffer.push_str(bean.trans_data());
961
962        // Send if buffer is full
963        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    // Send remaining data
971    if !buffer.is_empty() {
972        send_trace_message(&key_set, &buffer, trace_topic, producer, state).await?;
973    }
974
975    Ok(())
976}
977
978// Sends a single trace message to the broker.
979// Constructs the message with keys and body, uses MessageQueue selector for
980// round-robin queue selection, matching Java's behavior.
981async 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    // Use selector-based send for queue selection (matching Java)
1004    // Selector implements round-robin across available queues
1005    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        // Round-robin queue selection
1011        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    // The worker already runs off the application send path, so wait for the
1017    // trace send here to make shutdown/flush deterministic.
1018    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        // An abort is confirmed only when this current-thread runtime next polls the worker, and
1345        // the ticker completing does not order that poll before this assertion. Drain the group by
1346        // waiting for the task to leave its registry instead of assuming one scheduler turn.
1347        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); // Different counter values
1418    }
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        // Should succeed even before start (queue is available)
1427        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        // Test that batch_num is capped at 20
1435        let dispatcher = AsyncTraceDispatcher::new(
1436            test_runtime(),
1437            "TestGroup",
1438            Type::Produce,
1439            100, // Request 100
1440            "TRACE_TOPIC",
1441            None,
1442        );
1443
1444        assert_eq!(dispatcher.config.batch_num, 20); // Capped at 20
1445    }
1446
1447    #[test]
1448    fn test_default_trace_topic() {
1449        // Test default topic when empty string provided
1450        let dispatcher = AsyncTraceDispatcher::new(
1451            test_runtime(),
1452            "TestGroup",
1453            Type::Produce,
1454            20,
1455            "", // Empty topic
1456            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        // Should return error since not started
1505        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        // This test verifies the worker loop structure without actually starting
1549        // the producer (which requires a real broker connection).
1550        // The worker_loop will return an error because producer.start() fails,
1551        // but that's expected in a unit test environment.
1552
1553        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        // Close the channel immediately
1569        drop(tx);
1570
1571        // Worker will try to start producer and fail (expected in test environment)
1572        // The test just verifies no panic occurs
1573        let result = worker_loop(rx, state, producer, config).await;
1574
1575        // In unit test environment, start() will fail due to missing broker
1576        assert!(
1577            result.is_err(),
1578            "Expected error from producer start in test environment"
1579        );
1580    }
1581}