use cheetah_string::CheetahString;
use rocketmq_model::common::message::message_enum::MessageType;
use rocketmq_model::common::message::MessageConst;
use rocketmq_model::common::message::MessageTrait;
use rocketmq_model::common::mix_all;
use rocketmq_protocol::protocol::namespace_util::NamespaceUtil;
use rocketmq_runtime::common::time_utils::current_millis;
use crate::hook::end_transaction_context::EndTransactionContext;
use crate::hook::end_transaction_hook::EndTransactionHook;
use crate::trace::trace_bean::TraceBean;
use crate::trace::trace_context::TraceContext;
use crate::trace::trace_dispatcher::ArcTraceDispatcher;
use crate::trace::trace_type::TraceType;
pub struct EndTransactionTraceHookImpl {
trace_dispatcher: ArcTraceDispatcher,
}
impl EndTransactionTraceHookImpl {
pub fn new(trace_dispatcher: ArcTraceDispatcher) -> Self {
Self { trace_dispatcher }
}
}
impl EndTransactionHook for EndTransactionTraceHookImpl {
fn hook_name(&self) -> &'static str {
"EndTransactionTraceHook"
}
fn end_transaction(&self, context: &EndTransactionContext) {
let message = context.message;
if self
.trace_dispatcher
.trace_topic_name()
.is_some_and(|trace_topic| message.topic().starts_with(trace_topic))
{
return;
}
let mut trace_context = TraceContext::new();
trace_context.trace_type = Some(TraceType::EndTransaction);
trace_context.group_name =
CheetahString::from_string(NamespaceUtil::without_namespace(context.producer_group.as_str()));
trace_context.region_id = MessageTrait::property(
message,
&CheetahString::from_static_str(MessageConst::PROPERTY_MSG_REGION),
)
.filter(|region| !region.is_empty())
.unwrap_or_else(|| CheetahString::from_static_str(mix_all::DEFAULT_TRACE_REGION_ID));
trace_context.time_stamp = current_millis();
let mut trace_bean = TraceBean {
topic: CheetahString::from_string(NamespaceUtil::without_namespace(message.topic().as_str())),
tags: MessageTrait::tags(message).unwrap_or_default(),
keys: MessageTrait::get_keys(message).unwrap_or_default(),
store_host: context.broker_addr.clone(),
msg_type: Some(MessageType::TransMsgCommit),
msg_id: context.msg_id.clone(),
transaction_state: Some(context.transaction_state),
transaction_id: Some(context.transaction_id.clone()),
from_transaction_check: context.from_transaction_check,
..Default::default()
};
if let Some(client_host) = self.trace_dispatcher.trace_client_host() {
trace_bean.client_host = client_host;
}
trace_context.trace_beans = Some(vec![trace_bean]);
self.trace_dispatcher.append(&trace_context);
}
}
#[cfg(test)]
mod tests {
use std::any::Any;
use std::sync::Arc;
use std::sync::Mutex;
use rocketmq_model::common::message::message_single::Message;
use super::*;
use crate::base::access_channel::AccessChannel;
use crate::producer::local_transaction_state::LocalTransactionState;
use crate::trace::trace_dispatcher::TraceDispatcher;
struct CapturingTraceDispatcher {
trace_topic_name: &'static str,
client_host: Option<CheetahString>,
contexts: Mutex<Vec<TraceContext>>,
}
impl CapturingTraceDispatcher {
fn new(trace_topic_name: &'static str, client_host: Option<CheetahString>) -> Self {
Self {
trace_topic_name,
client_host,
contexts: Mutex::new(Vec::new()),
}
}
}
impl TraceDispatcher for CapturingTraceDispatcher {
fn start(&self, _name_srv_addr: &str, _access_channel: AccessChannel) -> crate::ClientResult<()> {
Ok(())
}
fn append(&self, ctx: &dyn Any) -> bool {
let Some(trace_context) = ctx.downcast_ref::<TraceContext>() else {
return false;
};
self.contexts.lock().expect("capture lock").push(trace_context.clone());
true
}
fn flush(&self) -> crate::ClientResult<()> {
Ok(())
}
fn shutdown(&self) {}
fn as_any(&self) -> &dyn Any {
self
}
fn as_mut_any(&mut self) -> &mut dyn Any {
self
}
fn trace_topic_name(&self) -> Option<&str> {
Some(self.trace_topic_name)
}
fn trace_client_host(&self) -> Option<CheetahString> {
self.client_host.clone()
}
}
fn transaction_message(topic: &str) -> Message {
let mut message = Message::builder()
.topic(topic)
.body_slice(b"transaction body")
.tags("tag-a")
.key("key-a")
.build_unchecked();
message.put_property(
CheetahString::from_static_str(MessageConst::PROPERTY_MSG_REGION),
CheetahString::from_static_str("RegionA"),
);
message
}
fn context<'a>(message: &'a Message) -> EndTransactionContext<'a> {
EndTransactionContext {
producer_group: CheetahString::from_static_str("ns%ProducerGroup"),
broker_addr: CheetahString::from_static_str("broker-a:10911"),
message,
msg_id: CheetahString::from_static_str("MSG_ID_001"),
transaction_id: CheetahString::from_static_str("TX_ID_001"),
transaction_state: LocalTransactionState::CommitMessage,
from_transaction_check: true,
}
}
#[test]
fn end_transaction_trace_hook_appends_java_compatible_context() {
let dispatcher = Arc::new(CapturingTraceDispatcher::new(
"TRACE_TOPIC",
Some(CheetahString::from_static_str("CLIENT_ID_001")),
));
let hook = EndTransactionTraceHookImpl::new(dispatcher.clone());
let message = transaction_message("ns%TxTopic");
hook.end_transaction(&context(&message));
let contexts = dispatcher.contexts.lock().expect("capture lock");
assert_eq!(contexts.len(), 1);
let trace_context = &contexts[0];
assert_eq!(trace_context.trace_type, Some(TraceType::EndTransaction));
assert_eq!(trace_context.group_name.as_str(), "ProducerGroup");
assert_eq!(trace_context.region_id.as_str(), "RegionA");
assert!(trace_context.time_stamp > 0);
let bean = &trace_context.trace_beans.as_ref().expect("trace beans")[0];
assert_eq!(bean.topic.as_str(), "TxTopic");
assert_eq!(bean.tags.as_str(), "tag-a");
assert_eq!(bean.keys.as_str(), "key-a");
assert_eq!(bean.store_host.as_str(), "broker-a:10911");
assert_eq!(bean.msg_type, Some(MessageType::TransMsgCommit));
assert_eq!(bean.client_host.as_str(), "CLIENT_ID_001");
assert_eq!(bean.msg_id.as_str(), "MSG_ID_001");
assert_eq!(bean.transaction_state, Some(LocalTransactionState::CommitMessage));
assert_eq!(bean.transaction_id.as_deref(), Some("TX_ID_001"));
assert!(bean.from_transaction_check);
}
#[test]
fn end_transaction_trace_hook_skips_trace_topic_like_java() {
let dispatcher = Arc::new(CapturingTraceDispatcher::new(
"TRACE_TOPIC",
Some(CheetahString::from_static_str("CLIENT_ID_001")),
));
let hook = EndTransactionTraceHookImpl::new(dispatcher.clone());
let message = transaction_message("TRACE_TOPIC");
hook.end_transaction(&context(&message));
assert!(dispatcher.contexts.lock().expect("capture lock").is_empty());
}
}