rocketmq_client_rust/stat/
consumer_stats_manager.rs1use std::future::Future;
16use std::sync::atomic::AtomicBool;
17use std::sync::atomic::Ordering;
18use std::sync::Arc;
19use std::thread;
20use std::time::Duration;
21
22use rocketmq_common::common::stats::stats_item_set::StatsItemSet;
23use rocketmq_remoting::protocol::body::consume_status::ConsumeStatus;
24use tracing::warn;
25
26const TOPIC_AND_GROUP_CONSUME_OK_TPS: &str = "CONSUME_OK_TPS";
27const TOPIC_AND_GROUP_CONSUME_FAILED_TPS: &str = "CONSUME_FAILED_TPS";
28const TOPIC_AND_GROUP_CONSUME_RT: &str = "CONSUME_RT";
29const TOPIC_AND_GROUP_PULL_TPS: &str = "PULL_TPS";
30const TOPIC_AND_GROUP_PULL_RT: &str = "PULL_RT";
31
32pub struct ConsumerStatsManager {
38 topic_and_group_consume_ok_tps: StatsItemSet,
39 topic_and_group_consume_rt: StatsItemSet,
40 topic_and_group_consume_failed_tps: StatsItemSet,
41 topic_and_group_pull_tps: StatsItemSet,
42 topic_and_group_pull_rt: StatsItemSet,
43 shutdown_signal: Arc<AtomicBool>,
44}
45
46impl Default for ConsumerStatsManager {
47 fn default() -> Self {
48 Self::new()
49 }
50}
51
52impl ConsumerStatsManager {
53 pub fn new() -> Self {
55 Self {
56 topic_and_group_consume_ok_tps: StatsItemSet::new(TOPIC_AND_GROUP_CONSUME_OK_TPS.to_string()),
57 topic_and_group_consume_rt: StatsItemSet::new(TOPIC_AND_GROUP_CONSUME_RT.to_string()),
58 topic_and_group_consume_failed_tps: StatsItemSet::new(TOPIC_AND_GROUP_CONSUME_FAILED_TPS.to_string()),
59 topic_and_group_pull_tps: StatsItemSet::new(TOPIC_AND_GROUP_PULL_TPS.to_string()),
60 topic_and_group_pull_rt: StatsItemSet::new(TOPIC_AND_GROUP_PULL_RT.to_string()),
61 shutdown_signal: Arc::new(AtomicBool::new(false)),
62 }
63 }
64
65 pub fn start(&self) {
74 self.shutdown_signal.store(false, Ordering::Release);
75
76 let sets_sec = [
78 self.topic_and_group_consume_ok_tps.clone(),
79 self.topic_and_group_consume_rt.clone(),
80 self.topic_and_group_consume_failed_tps.clone(),
81 self.topic_and_group_pull_tps.clone(),
82 self.topic_and_group_pull_rt.clone(),
83 ];
84 let shutdown_signal = self.shutdown_signal.clone();
85 spawn_stats_task("rocketmq-client-consumer-stats-seconds", async move {
86 loop {
87 if shutdown_signal.load(Ordering::Acquire) {
88 break;
89 }
90 for set in &sets_sec {
91 set.sampling_in_seconds();
92 }
93 if !sleep_or_shutdown(&shutdown_signal, Duration::from_secs(10)).await {
94 break;
95 }
96 }
97 });
98
99 let sets_min = [
101 self.topic_and_group_consume_ok_tps.clone(),
102 self.topic_and_group_consume_rt.clone(),
103 self.topic_and_group_consume_failed_tps.clone(),
104 self.topic_and_group_pull_tps.clone(),
105 self.topic_and_group_pull_rt.clone(),
106 ];
107 let shutdown_signal = self.shutdown_signal.clone();
108 spawn_stats_task("rocketmq-client-consumer-stats-minutes", async move {
109 loop {
110 if shutdown_signal.load(Ordering::Acquire) {
111 break;
112 }
113 for set in &sets_min {
114 set.sampling_in_minutes();
115 }
116 if !sleep_or_shutdown(&shutdown_signal, Duration::from_secs(10 * 60)).await {
117 break;
118 }
119 }
120 });
121
122 let sets_hour = [
124 self.topic_and_group_consume_ok_tps.clone(),
125 self.topic_and_group_consume_rt.clone(),
126 self.topic_and_group_consume_failed_tps.clone(),
127 self.topic_and_group_pull_tps.clone(),
128 self.topic_and_group_pull_rt.clone(),
129 ];
130 let shutdown_signal = self.shutdown_signal.clone();
131 spawn_stats_task("rocketmq-client-consumer-stats-hours", async move {
132 loop {
133 if shutdown_signal.load(Ordering::Acquire) {
134 break;
135 }
136 for set in &sets_hour {
137 set.sampling_in_hours();
138 }
139 if !sleep_or_shutdown(&shutdown_signal, Duration::from_secs(3600)).await {
140 break;
141 }
142 }
143 });
144 }
145
146 pub fn shutdown(&self) {
148 self.shutdown_signal.store(true, Ordering::Release);
149 }
150
151 pub fn inc_pull_rt(&self, group: &str, topic: &str, rt: u64) {
153 self.topic_and_group_pull_rt
154 .add_rt_value(&stats_key(topic, group), rt as i64, 1);
155 }
156
157 pub fn inc_pull_tps(&self, group: &str, topic: &str, msgs: u64) {
159 self.topic_and_group_pull_tps
160 .add_value(&stats_key(topic, group), msgs as i64, 1);
161 }
162
163 pub fn inc_consume_rt(&self, group: &str, topic: &str, rt: u64) {
165 self.topic_and_group_consume_rt
166 .add_rt_value(&stats_key(topic, group), rt as i64, 1);
167 }
168
169 pub fn inc_consume_ok_tps(&self, group: &str, topic: &str, msgs: u64) {
171 self.topic_and_group_consume_ok_tps
172 .add_value(&stats_key(topic, group), msgs as i64, 1);
173 }
174
175 #[inline]
177 pub fn inc_consume_oktps(&self, group: &str, topic: &str, msgs: u64) {
178 self.inc_consume_ok_tps(group, topic, msgs);
179 }
180
181 pub fn inc_consume_failed_tps(&self, group: &str, topic: &str, msgs: u64) {
183 self.topic_and_group_consume_failed_tps
184 .add_value(&stats_key(topic, group), msgs as i64, 1);
185 }
186
187 pub fn consume_status(&self, group: &str, topic: &str) -> ConsumeStatus {
194 let key = stats_key(topic, group);
195
196 let pull_rt = self.topic_and_group_pull_rt.get_stats_data_in_minute(&key).get_avgpt();
197
198 let pull_tps = self.topic_and_group_pull_tps.get_stats_data_in_minute(&key).get_tps();
199
200 let consume_rt = {
201 let minute = self.topic_and_group_consume_rt.get_stats_data_in_minute(&key);
202 if minute.get_sum() == 0 {
203 self.topic_and_group_consume_rt.get_stats_data_in_hour(&key).get_avgpt()
204 } else {
205 minute.get_avgpt()
206 }
207 };
208
209 let consume_ok_tps = self
210 .topic_and_group_consume_ok_tps
211 .get_stats_data_in_minute(&key)
212 .get_tps();
213
214 let consume_failed_tps = self
215 .topic_and_group_consume_failed_tps
216 .get_stats_data_in_minute(&key)
217 .get_tps();
218
219 let consume_failed_msgs = self
220 .topic_and_group_consume_failed_tps
221 .get_stats_data_in_hour(&key)
222 .get_sum() as i64;
223
224 ConsumeStatus {
225 pull_rt,
226 pull_tps,
227 consume_rt,
228 consume_ok_tps,
229 consume_failed_tps,
230 consume_failed_msgs,
231 }
232 }
233}
234
235#[inline]
237fn stats_key(topic: &str, group: &str) -> String {
238 format!("{topic}@{group}")
239}
240
241fn spawn_stats_task<F>(thread_name: &'static str, task: F)
242where
243 F: Future<Output = ()> + Send + 'static,
244{
245 if let Ok(handle) = tokio::runtime::Handle::try_current() {
246 drop(handle.spawn(task));
247 return;
248 }
249
250 match tokio::runtime::Builder::new_current_thread().enable_all().build() {
251 Ok(runtime) => {
252 if let Err(error) = thread::Builder::new()
253 .name(thread_name.to_string())
254 .spawn(move || runtime.block_on(task))
255 {
256 warn!("Failed to spawn {} background thread: {}", thread_name, error);
257 }
258 }
259 Err(error) => warn!("Failed to build {} runtime: {}", thread_name, error),
260 }
261}
262
263async fn sleep_or_shutdown(shutdown_signal: &Arc<AtomicBool>, delay: Duration) -> bool {
264 let deadline = tokio::time::Instant::now() + delay;
265
266 loop {
267 if shutdown_signal.load(Ordering::Acquire) {
268 return false;
269 }
270
271 let now = tokio::time::Instant::now();
272 if now >= deadline {
273 return true;
274 }
275
276 tokio::time::sleep((deadline - now).min(Duration::from_millis(50))).await;
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 fn make_manager() -> ConsumerStatsManager {
285 ConsumerStatsManager::new()
286 }
287
288 #[test]
289 fn stats_key_format() {
290 assert_eq!(stats_key("TopicA", "GroupA"), "TopicA@GroupA");
291 }
292
293 #[test]
294 fn smoke_inc_consume_ok_tps() {
295 let mgr = make_manager();
296 mgr.inc_consume_ok_tps("GroupA", "TopicA", 5);
297 }
298
299 #[test]
300 fn java_acronym_alias_inc_consume_oktps_records_ok_tps() {
301 let mgr = make_manager();
302 mgr.inc_consume_oktps("GroupA", "TopicA", 5);
303 let item = mgr
304 .topic_and_group_consume_ok_tps
305 .get_stats_item(&stats_key("TopicA", "GroupA"))
306 .expect("alias should record the same stats item as inc_consume_ok_tps");
307
308 assert_eq!(item.get_value(), 5);
309 assert_eq!(item.get_times(), 1);
310 }
311
312 #[test]
313 fn smoke_inc_consume_failed_tps() {
314 let mgr = make_manager();
315 mgr.inc_consume_failed_tps("GroupA", "TopicA", 3);
316 }
317
318 #[test]
319 fn smoke_inc_consume_rt() {
320 let mgr = make_manager();
321 mgr.inc_consume_rt("GroupA", "TopicA", 42);
322 }
323
324 #[test]
325 fn smoke_inc_pull_rt() {
326 let mgr = make_manager();
327 mgr.inc_pull_rt("GroupA", "TopicA", 10);
328 }
329
330 #[test]
331 fn smoke_inc_pull_tps() {
332 let mgr = make_manager();
333 mgr.inc_pull_tps("GroupA", "TopicA", 100);
334 }
335
336 #[test]
337 fn consume_status_returns_zero_for_empty_stats() {
338 let mgr = make_manager();
339 let status = mgr.consume_status("GroupA", "TopicA");
340 assert_eq!(status.pull_rt, 0.0);
341 assert_eq!(status.pull_tps, 0.0);
342 assert_eq!(status.consume_rt, 0.0);
343 assert_eq!(status.consume_ok_tps, 0.0);
344 assert_eq!(status.consume_failed_tps, 0.0);
345 assert_eq!(status.consume_failed_msgs, 0);
346 }
347
348 #[tokio::test]
349 async fn start_launches_background_tasks() {
350 let mgr = make_manager();
351 mgr.start();
353 mgr.shutdown();
354 }
355
356 #[test]
357 fn start_without_tokio_runtime_does_not_panic() {
358 let mgr = make_manager();
359 mgr.start();
360 mgr.shutdown();
361 thread::sleep(Duration::from_millis(80));
362 }
363
364 #[test]
365 fn default_creates_valid_manager() {
366 let mgr = ConsumerStatsManager::default();
367 let _ = mgr.consume_status("G", "T");
369 }
370}