1use std::collections::HashMap;
16use std::collections::HashSet;
17use std::fmt;
18use std::fmt::Display;
19
20use cheetah_string::CheetahString;
21use serde::Deserialize;
22use serde::Deserializer;
23use serde::Serialize;
24use serde::Serializer;
25
26const DEFAULT_TOPIC_PERMISSION: u32 = 0x1 << 2 | 0x1 << 1;
27const MESSAGE_TYPE_ATTRIBUTE: &str = "message.type";
28const LITE_EXPIRATION_ATTRIBUTE: &str = "lite.topic.expiration";
29
30pub const RETRY_GROUP_TOPIC_PREFIX: &str = "%RETRY%";
31pub const DLQ_GROUP_TOPIC_PREFIX: &str = "%DLQ%";
32pub const SYSTEM_TOPIC_PREFIX: &str = "rmq_sys_";
33pub const SYSTEM_TOPICS: &[&str] = &[
34 "SCHEDULE_TOPIC_XXXX",
35 "BenchmarkTest",
36 "RMQ_SYS_TRANS_HALF_TOPIC",
37 "RMQ_SYS_ROCKSDB_TRANS_HALF_TOPIC",
38 "RMQ_SYS_TRACE_TOPIC",
39 "RMQ_SYS_TRANS_OP_HALF_TOPIC",
40 "RMQ_SYS_ROCKSDB_TRANS_OP_HALF_TOPIC",
41 "TRANS_CHECK_MAX_TIME_TOPIC",
42 "SELF_TEST_TOPIC",
43 "OFFSET_MOVED_EVENT",
44 "CHECKPOINT_TOPIC",
45];
46
47pub fn is_system_topic(topic: &str) -> bool {
49 topic.starts_with(SYSTEM_TOPIC_PREFIX) || SYSTEM_TOPICS.contains(&topic)
50}
51
52#[derive(Clone, Default, Eq, PartialEq, Copy)]
54pub enum TopicFilterType {
55 #[default]
56 SingleTag,
57 MultiTag,
58}
59
60impl TopicFilterType {
61 pub const fn as_str(&self) -> &'static str {
62 match self {
63 Self::SingleTag => "SINGLE_TAG",
64 Self::MultiTag => "MULTI_TAG",
65 }
66 }
67}
68
69impl Display for TopicFilterType {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 f.write_str(self.as_str())
72 }
73}
74
75impl fmt::Debug for TopicFilterType {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 f.write_str(self.as_str())
78 }
79}
80
81impl From<&str> for TopicFilterType {
82 fn from(value: &str) -> Self {
83 match value {
84 "MULTI_TAG" => Self::MultiTag,
85 _ => Self::SingleTag,
86 }
87 }
88}
89
90impl From<String> for TopicFilterType {
91 fn from(value: String) -> Self {
92 Self::from(value.as_str())
93 }
94}
95
96impl From<i32> for TopicFilterType {
97 fn from(value: i32) -> Self {
98 match value {
99 1 => Self::MultiTag,
100 _ => Self::SingleTag,
101 }
102 }
103}
104
105impl Serialize for TopicFilterType {
106 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
107 where
108 S: Serializer,
109 {
110 serializer.serialize_str(self.as_str())
111 }
112}
113
114impl<'de> Deserialize<'de> for TopicFilterType {
115 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
116 where
117 D: Deserializer<'de>,
118 {
119 let value = String::deserialize(deserializer)?;
120 match value.as_str() {
121 "SINGLE_TAG" => Ok(Self::SingleTag),
122 "MULTI_TAG" => Ok(Self::MultiTag),
123 _ => Err(serde::de::Error::unknown_variant(&value, &["SINGLE_TAG", "MULTI_TAG"])),
124 }
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
130pub enum TopicMessageType {
131 Unspecified,
132 Normal,
133 Fifo,
134 Delay,
135 Transaction,
136 Priority,
137 Lite,
138 Mixed,
139}
140
141impl From<String> for TopicMessageType {
142 fn from(value: String) -> Self {
143 match value.to_uppercase().as_str() {
144 "UNSPECIFIED" => Self::Unspecified,
145 "NORMAL" => Self::Normal,
146 "FIFO" => Self::Fifo,
147 "DELAY" => Self::Delay,
148 "TRANSACTION" => Self::Transaction,
149 "PRIORITY" => Self::Priority,
150 "LITE" => Self::Lite,
151 "MIXED" => Self::Mixed,
152 _ => Self::Unspecified,
153 }
154 }
155}
156
157impl TopicMessageType {
158 pub const fn as_str(&self) -> &'static str {
159 match self {
160 Self::Unspecified => "UNSPECIFIED",
161 Self::Normal => "NORMAL",
162 Self::Fifo => "FIFO",
163 Self::Delay => "DELAY",
164 Self::Transaction => "TRANSACTION",
165 Self::Priority => "PRIORITY",
166 Self::Lite => "LITE",
167 Self::Mixed => "MIXED",
168 }
169 }
170
171 pub fn topic_message_type_set() -> HashSet<String> {
172 [
173 Self::Unspecified,
174 Self::Normal,
175 Self::Fifo,
176 Self::Delay,
177 Self::Transaction,
178 Self::Priority,
179 Self::Lite,
180 Self::Mixed,
181 ]
182 .into_iter()
183 .map(|value| value.as_str().to_owned())
184 .collect()
185 }
186
187 pub fn parse_from_message_property(properties: &HashMap<String, String>) -> Self {
188 if properties
189 .get("TRAN_MSG")
190 .is_some_and(|value| value.eq_ignore_ascii_case("true"))
191 {
192 Self::Transaction
193 } else if ["DELAY", "TIMER_DELIVER_MS", "TIMER_DELAY_SEC", "TIMER_DELAY_MS"]
194 .iter()
195 .any(|key| properties.contains_key(*key))
196 {
197 Self::Delay
198 } else if properties.contains_key("__SHARDINGKEY") {
199 Self::Fifo
200 } else if properties.contains_key("_SYS_MSG_PRIORITY_") {
201 Self::Priority
202 } else if properties.contains_key("__LITE_TOPIC") {
203 Self::Lite
204 } else {
205 Self::Normal
206 }
207 }
208
209 pub fn get_metrics_value(&self) -> String {
210 self.as_str().to_ascii_lowercase()
211 }
212}
213
214impl Display for TopicMessageType {
215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216 f.write_str(self.as_str())
217 }
218}
219
220#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
222#[serde(rename_all = "camelCase")]
223pub struct TopicConfig {
224 pub topic_name: Option<CheetahString>,
225 pub read_queue_nums: u32,
226 pub write_queue_nums: u32,
227 pub perm: u32,
228 pub topic_filter_type: TopicFilterType,
229 pub topic_sys_flag: u32,
230 pub order: bool,
231 pub attributes: HashMap<CheetahString, CheetahString>,
232}
233
234impl TopicConfig {
235 const DEFAULT_READ_QUEUE_NUMS: u32 = 16;
236 const DEFAULT_WRITE_QUEUE_NUMS: u32 = 16;
237 const SEPARATOR: &'static str = " ";
238
239 pub fn new(topic_name: impl Into<CheetahString>) -> Self {
240 Self {
241 topic_name: Some(topic_name.into()),
242 ..Self::default()
243 }
244 }
245
246 pub fn with_queues(topic_name: impl Into<CheetahString>, read_queue_nums: u32, write_queue_nums: u32) -> Self {
247 Self {
248 read_queue_nums,
249 write_queue_nums,
250 ..Self::new(topic_name)
251 }
252 }
253
254 pub fn with_perm(
255 topic_name: impl Into<CheetahString>,
256 read_queue_nums: u32,
257 write_queue_nums: u32,
258 perm: u32,
259 ) -> Self {
260 Self {
261 read_queue_nums,
262 write_queue_nums,
263 perm,
264 ..Self::new(topic_name)
265 }
266 }
267
268 pub fn with_sys_flag(
269 topic_name: impl Into<CheetahString>,
270 read_queue_nums: u32,
271 write_queue_nums: u32,
272 perm: u32,
273 topic_sys_flag: u32,
274 ) -> Self {
275 Self {
276 read_queue_nums,
277 write_queue_nums,
278 perm,
279 topic_sys_flag,
280 ..Self::new(topic_name)
281 }
282 }
283
284 pub fn get_topic_message_type(&self) -> TopicMessageType {
285 self.attributes
286 .get(MESSAGE_TYPE_ATTRIBUTE)
287 .map(|value| TopicMessageType::from(value.to_string()))
288 .unwrap_or(TopicMessageType::Normal)
289 }
290
291 pub fn set_lite_topic_expiration(&mut self, lite_topic_expiration: i32) {
292 if self.get_topic_message_type() == TopicMessageType::Lite {
293 self.attributes.insert(
294 CheetahString::from_static_str(LITE_EXPIRATION_ATTRIBUTE),
295 CheetahString::from_string(lite_topic_expiration.to_string()),
296 );
297 }
298 }
299
300 pub fn get_lite_topic_expiration(&self) -> i32 {
301 if self.get_topic_message_type() != TopicMessageType::Lite {
302 return -1;
303 }
304 self.attributes
305 .get(LITE_EXPIRATION_ATTRIBUTE)
306 .and_then(|value| value.parse().ok())
307 .unwrap_or(-1)
308 }
309
310 pub fn encode(&self) -> String {
311 let mut parts = vec![
312 self.topic_name.clone().unwrap_or_default().to_string(),
313 self.read_queue_nums.to_string(),
314 self.write_queue_nums.to_string(),
315 self.perm.to_string(),
316 self.topic_filter_type.to_string(),
317 ];
318 if !self.attributes.is_empty() {
319 parts.push(serde_json::to_string(&self.attributes).unwrap_or_default());
320 }
321 parts.join(Self::SEPARATOR)
322 }
323
324 pub fn decode(&mut self, input: &str) -> bool {
325 let parts: Vec<&str> = input.splitn(6, Self::SEPARATOR).collect();
326 if parts.len() < 5 {
327 return false;
328 }
329 self.topic_name = Some(parts[0].into());
330 self.read_queue_nums = parts[1].parse().unwrap_or(Self::DEFAULT_READ_QUEUE_NUMS);
331 self.write_queue_nums = parts[2].parse().unwrap_or(Self::DEFAULT_WRITE_QUEUE_NUMS);
332 self.perm = parts[3].parse().unwrap_or(DEFAULT_TOPIC_PERMISSION);
333 self.topic_filter_type = parts[4].into();
334 if let Some(attributes) = parts.get(5).and_then(|value| serde_json::from_str(value).ok()) {
335 self.attributes = attributes;
336 }
337 true
338 }
339
340 pub fn get_read_queue_nums(&self) -> u32 {
341 self.read_queue_nums
342 }
343}
344
345impl Default for TopicConfig {
346 fn default() -> Self {
347 Self {
348 topic_name: None,
349 read_queue_nums: Self::DEFAULT_READ_QUEUE_NUMS,
350 write_queue_nums: Self::DEFAULT_WRITE_QUEUE_NUMS,
351 perm: DEFAULT_TOPIC_PERMISSION,
352 topic_filter_type: TopicFilterType::SingleTag,
353 topic_sys_flag: 0,
354 order: false,
355 attributes: HashMap::new(),
356 }
357 }
358}
359
360impl Display for TopicConfig {
361 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362 write!(
363 f,
364 "TopicConfig {{ topic_name: {:?}, read_queue_nums: {}, write_queue_nums: {}, perm: {}, topic_filter_type: \
365 {}, topic_sys_flag: {}, order: {}, attributes: {:?} }}",
366 self.topic_name,
367 self.read_queue_nums,
368 self.write_queue_nums,
369 self.perm,
370 self.topic_filter_type,
371 self.topic_sys_flag,
372 self.order,
373 self.attributes
374 )
375 }
376}
377
378#[cfg(test)]
379mod topic_message_type_tests {
380 use std::collections::HashMap;
381
382 use super::*;
383
384 type PrecedenceCase = (&'static str, &'static [(&'static str, &'static str)], TopicMessageType);
387
388 fn props(pairs: &[(&str, &str)]) -> HashMap<String, String> {
389 pairs
390 .iter()
391 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
392 .collect()
393 }
394
395 #[test]
396 fn parse_from_message_property_marker_precedence() {
397 let cases: &[PrecedenceCase] = &[
400 (
401 "TRAN_MSG=true outranks delay and sharding markers",
402 &[("TRAN_MSG", "true"), ("DELAY", "1"), ("__SHARDINGKEY", "k")],
403 TopicMessageType::Transaction,
404 ),
405 (
406 "a delay marker outranks sharding/priority/lite; TRAN_MSG=false does not force Transaction",
407 &[
408 ("TRAN_MSG", "false"),
409 ("DELAY", "3"),
410 ("__SHARDINGKEY", "k"),
411 ("_SYS_MSG_PRIORITY_", "5"),
412 ("__LITE_TOPIC", "1"),
413 ],
414 TopicMessageType::Delay,
415 ),
416 (
417 "__SHARDINGKEY outranks priority and lite",
418 &[
419 ("__SHARDINGKEY", "k"),
420 ("_SYS_MSG_PRIORITY_", "5"),
421 ("__LITE_TOPIC", "1"),
422 ],
423 TopicMessageType::Fifo,
424 ),
425 (
426 "_SYS_MSG_PRIORITY_ outranks lite",
427 &[("_SYS_MSG_PRIORITY_", "5"), ("__LITE_TOPIC", "1")],
428 TopicMessageType::Priority,
429 ),
430 (
431 "__LITE_TOPIC alone selects Lite",
432 &[("__LITE_TOPIC", "1")],
433 TopicMessageType::Lite,
434 ),
435 (
436 "TRAN_MSG=false with no other marker selects Normal",
437 &[("TRAN_MSG", "false")],
438 TopicMessageType::Normal,
439 ),
440 ("an empty property map selects Normal", &[], TopicMessageType::Normal),
441 ];
442
443 for (desc, pairs, expected) in cases {
444 let got = TopicMessageType::parse_from_message_property(&props(pairs));
445 assert_eq!(got, *expected, "{desc}");
446 }
447 }
448
449 #[test]
450 fn parse_from_message_property_treats_every_timer_key_as_delay() {
451 for key in ["DELAY", "TIMER_DELIVER_MS", "TIMER_DELAY_SEC", "TIMER_DELAY_MS"] {
452 let got = TopicMessageType::parse_from_message_property(&props(&[(key, "1")]));
453 assert_eq!(got, TopicMessageType::Delay, "{key} should select Delay");
454 }
455 }
456
457 #[test]
458 fn parse_from_message_property_tran_msg_true_is_case_insensitive() {
459 for value in ["true", "TRUE", "TrUe"] {
460 let got = TopicMessageType::parse_from_message_property(&props(&[("TRAN_MSG", value)]));
461 assert_eq!(
462 got,
463 TopicMessageType::Transaction,
464 "TRAN_MSG={value} should select Transaction"
465 );
466 }
467 let got = TopicMessageType::parse_from_message_property(&props(&[("TRAN_MSG", "1")]));
469 assert_eq!(got, TopicMessageType::Normal, "TRAN_MSG=1 is not Transaction");
470 }
471}