1use std::collections::HashMap;
16use std::collections::HashSet;
17
18use cheetah_string::CheetahString;
19use rocketmq_model::common::attribute::subscription_group_attributes::LITE_BIND_TOPIC_ATTRIBUTE_NAME;
20use rocketmq_model::common::attribute::subscription_group_attributes::LITE_SUB_CLIENT_MAX_EVENT_COUNT_ATTRIBUTE_NAME;
21use rocketmq_model::common::attribute::subscription_group_attributes::LITE_SUB_CLIENT_QUOTA_ATTRIBUTE_NAME;
22use rocketmq_model::common::attribute::subscription_group_attributes::LITE_SUB_MODEL_ATTRIBUTE_NAME;
23use rocketmq_model::common::attribute::subscription_group_attributes::LITE_SUB_RESET_OFFSET_EXCLUSIVE_ATTRIBUTE_NAME;
24use rocketmq_model::common::attribute::subscription_group_attributes::LITE_SUB_RESET_OFFSET_UNSUBSCRIBE_ATTRIBUTE_NAME;
25use rocketmq_model::common::attribute::subscription_group_attributes::LITE_SUB_WILDCARD_ATTRIBUTE_NAME;
26use rocketmq_model::common::attribute::subscription_group_attributes::PRIORITY_FACTOR_ATTRIBUTE_NAME;
27use rocketmq_model::common::topic::TopicValidator;
28use serde::Deserialize;
29use serde::Serialize;
30
31use crate::common::wire_constants::MASTER_ID;
32use crate::protocol::subscription::group_retry_policy::GroupRetryPolicy;
33use crate::protocol::subscription::simple_subscription_data::SimpleSubscriptionData;
34use crate::ProtocolContractViolation;
35
36pub const SUBSCRIPTION_GROUP_NAME_MAX_LENGTH: usize = 255;
37
38pub fn validate_subscription_group_name(group_name: &str) -> Result<(), ProtocolContractViolation> {
45 if group_name.trim().is_empty() {
46 return Err(ProtocolContractViolation::BlankSubscriptionGroup);
47 }
48 if TopicValidator::is_topic_or_group_illegal(group_name) {
49 return Err(ProtocolContractViolation::SubscriptionGroupIllegalCharacters);
50 }
51 if group_name.len() > SUBSCRIPTION_GROUP_NAME_MAX_LENGTH {
52 return Err(ProtocolContractViolation::SubscriptionGroupTooLong {
53 max_length: SUBSCRIPTION_GROUP_NAME_MAX_LENGTH,
54 });
55 }
56 Ok(())
57}
58
59pub fn validate_subscription_group_configs(
66 configs: &[SubscriptionGroupConfig],
67) -> Result<(), ProtocolContractViolation> {
68 let mut seen = HashSet::with_capacity(configs.len());
69 for config in configs {
70 validate_subscription_group_name(config.group_name().as_str())?;
71 if !seen.insert(config.group_name().clone()) {
72 return Err(ProtocolContractViolation::DuplicateSubscriptionGroup);
73 }
74 }
75 Ok(())
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(default, rename_all = "camelCase")]
80pub struct SubscriptionGroupConfig {
81 group_name: CheetahString,
82
83 consume_enable: bool,
84 consume_from_min_enable: bool,
85 consume_broadcast_enable: bool,
86 consume_message_orderly: bool,
87
88 retry_queue_nums: i32,
89 retry_max_times: i32,
90 group_retry_policy: GroupRetryPolicy,
91
92 broker_id: u64,
93 which_broker_when_consume_slowly: u64,
94
95 notify_consumer_ids_changed_enable: bool,
96
97 group_sys_flag: i32,
98
99 consume_timeout_minute: i32,
100
101 subscription_data_set: Option<HashSet<SimpleSubscriptionData>>,
102 attributes: HashMap<CheetahString, CheetahString>,
103}
104
105impl SubscriptionGroupConfig {
106 pub fn new(group_name: CheetahString) -> Self {
107 Self {
108 group_name,
109 ..Default::default()
110 }
111 }
112}
113
114impl Default for SubscriptionGroupConfig {
115 fn default() -> Self {
116 SubscriptionGroupConfig {
117 group_name: CheetahString::default(),
118
119 consume_enable: true,
120 consume_from_min_enable: true,
121 consume_broadcast_enable: true,
122 consume_message_orderly: false,
123
124 retry_queue_nums: 1,
125 retry_max_times: 16,
126 group_retry_policy: GroupRetryPolicy::default(),
127
128 broker_id: MASTER_ID,
129 which_broker_when_consume_slowly: 1,
130
131 notify_consumer_ids_changed_enable: true,
132
133 group_sys_flag: 0,
134
135 consume_timeout_minute: 15,
136
137 subscription_data_set: None,
138 attributes: HashMap::new(),
139 }
140 }
141}
142
143impl SubscriptionGroupConfig {
144 #[inline]
145 pub fn group_name(&self) -> &CheetahString {
146 &self.group_name
147 }
148
149 #[inline]
150 pub fn consume_enable(&self) -> bool {
151 self.consume_enable
152 }
153
154 #[inline]
155 pub fn consume_from_min_enable(&self) -> bool {
156 self.consume_from_min_enable
157 }
158
159 #[inline]
160 pub fn consume_broadcast_enable(&self) -> bool {
161 self.consume_broadcast_enable
162 }
163
164 #[inline]
165 pub fn consume_message_orderly(&self) -> bool {
166 self.consume_message_orderly
167 }
168
169 #[inline]
170 pub fn retry_queue_nums(&self) -> i32 {
171 self.retry_queue_nums
172 }
173
174 #[inline]
175 pub fn retry_max_times(&self) -> i32 {
176 self.retry_max_times
177 }
178
179 #[inline]
180 pub fn group_retry_policy(&self) -> &GroupRetryPolicy {
181 &self.group_retry_policy
182 }
183
184 #[inline]
185 pub fn broker_id(&self) -> u64 {
186 self.broker_id
187 }
188
189 #[inline]
190 pub fn which_broker_when_consume_slowly(&self) -> u64 {
191 self.which_broker_when_consume_slowly
192 }
193
194 #[inline]
195 pub fn notify_consumer_ids_changed_enable(&self) -> bool {
196 self.notify_consumer_ids_changed_enable
197 }
198
199 #[inline]
200 pub fn group_sys_flag(&self) -> i32 {
201 self.group_sys_flag
202 }
203
204 #[inline]
205 pub fn consume_timeout_minute(&self) -> i32 {
206 self.consume_timeout_minute
207 }
208
209 #[inline]
210 pub fn subscription_data_set(&self) -> Option<&HashSet<SimpleSubscriptionData>> {
211 self.subscription_data_set.as_ref()
212 }
213
214 #[inline]
215 pub fn attributes(&self) -> &HashMap<CheetahString, CheetahString> {
216 &self.attributes
217 }
218
219 #[inline]
220 pub fn lite_bind_topic(&self) -> Option<&CheetahString> {
221 self.attributes
222 .get(&CheetahString::from_static_str(LITE_BIND_TOPIC_ATTRIBUTE_NAME))
223 .filter(|value| !value.is_empty())
224 }
225
226 #[inline]
227 pub fn lite_sub_client_quota(&self) -> i32 {
228 self.attributes
229 .get(&CheetahString::from_static_str(LITE_SUB_CLIENT_QUOTA_ATTRIBUTE_NAME))
230 .and_then(|value| value.parse::<i64>().ok())
231 .unwrap_or(2000) as i32
232 }
233
234 #[inline]
235 pub fn lite_sub_exclusive(&self) -> bool {
236 self.attributes
237 .get(&CheetahString::from_static_str(LITE_SUB_MODEL_ATTRIBUTE_NAME))
238 .is_some_and(|value| value == "Exclusive")
239 }
240
241 #[inline]
242 pub fn max_client_event_count(&self) -> i32 {
243 self.attributes
244 .get(&CheetahString::from_static_str(
245 LITE_SUB_CLIENT_MAX_EVENT_COUNT_ATTRIBUTE_NAME,
246 ))
247 .and_then(|value| value.parse::<i32>().ok())
248 .unwrap_or(-1)
249 }
250
251 #[inline]
256 pub fn lite_sub_wildcard(&self) -> bool {
257 self.attributes
258 .contains_key(&CheetahString::from_static_str(LITE_SUB_WILDCARD_ATTRIBUTE_NAME))
259 }
260
261 #[inline]
262 pub fn set_lite_sub_wildcard(&mut self, wildcard: bool) {
263 let key = CheetahString::from_static_str(LITE_SUB_WILDCARD_ATTRIBUTE_NAME);
264 if wildcard {
265 self.attributes.insert(key, CheetahString::from_static_str("true"));
266 } else {
267 self.attributes.remove(&key);
268 }
269 }
270
271 #[inline]
272 pub fn reset_offset_in_exclusive_mode(&self) -> bool {
273 self.attributes
274 .get(&CheetahString::from_static_str(
275 LITE_SUB_RESET_OFFSET_EXCLUSIVE_ATTRIBUTE_NAME,
276 ))
277 .is_some_and(|value| value.parse::<bool>().unwrap_or(false))
278 }
279
280 #[inline]
281 pub fn reset_offset_on_unsubscribe(&self) -> bool {
282 self.attributes
283 .get(&CheetahString::from_static_str(
284 LITE_SUB_RESET_OFFSET_UNSUBSCRIBE_ATTRIBUTE_NAME,
285 ))
286 .is_some_and(|value| value.parse::<bool>().unwrap_or(false))
287 }
288
289 #[inline]
290 pub fn priority_factor(&self) -> i32 {
291 self.attributes
292 .get(&CheetahString::from_static_str(PRIORITY_FACTOR_ATTRIBUTE_NAME))
293 .and_then(|value| value.parse::<i32>().ok())
294 .unwrap_or(100)
295 }
296
297 #[inline]
298 pub fn set_group_name(&mut self, group_name: CheetahString) {
299 self.group_name = group_name;
300 }
301
302 #[inline]
303 pub fn set_consume_enable(&mut self, consume_enable: bool) {
304 self.consume_enable = consume_enable;
305 }
306
307 #[inline]
308 pub fn set_consume_from_min_enable(&mut self, consume_from_min_enable: bool) {
309 self.consume_from_min_enable = consume_from_min_enable;
310 }
311
312 #[inline]
313 pub fn set_consume_broadcast_enable(&mut self, consume_broadcast_enable: bool) {
314 self.consume_broadcast_enable = consume_broadcast_enable;
315 }
316
317 #[inline]
318 pub fn set_consume_message_orderly(&mut self, consume_message_orderly: bool) {
319 self.consume_message_orderly = consume_message_orderly;
320 }
321
322 #[inline]
323 pub fn set_retry_queue_nums(&mut self, retry_queue_nums: i32) {
324 self.retry_queue_nums = retry_queue_nums;
325 }
326
327 #[inline]
328 pub fn set_retry_max_times(&mut self, retry_max_times: i32) {
329 self.retry_max_times = retry_max_times;
330 }
331
332 #[inline]
333 pub fn set_group_retry_policy(&mut self, group_retry_policy: GroupRetryPolicy) {
334 self.group_retry_policy = group_retry_policy;
335 }
336
337 #[inline]
338 pub fn set_broker_id(&mut self, broker_id: u64) {
339 self.broker_id = broker_id;
340 }
341
342 #[inline]
343 pub fn set_which_broker_when_consume_slowly(&mut self, which_broker_when_consume_slowly: u64) {
344 self.which_broker_when_consume_slowly = which_broker_when_consume_slowly;
345 }
346
347 #[inline]
348 pub fn set_notify_consumer_ids_changed_enable(&mut self, notify_consumer_ids_changed_enable: bool) {
349 self.notify_consumer_ids_changed_enable = notify_consumer_ids_changed_enable;
350 }
351
352 #[inline]
353 pub fn set_group_sys_flag(&mut self, group_sys_flag: i32) {
354 self.group_sys_flag = group_sys_flag;
355 }
356
357 #[inline]
358 pub fn set_consume_timeout_minute(&mut self, consume_timeout_minute: i32) {
359 self.consume_timeout_minute = consume_timeout_minute;
360 }
361
362 #[inline]
363 pub fn set_subscription_data_set(&mut self, subscription_data_set: Option<HashSet<SimpleSubscriptionData>>) {
364 self.subscription_data_set = subscription_data_set;
365 }
366
367 #[inline]
368 pub fn set_attributes(&mut self, attributes: HashMap<CheetahString, CheetahString>) {
369 self.attributes = attributes;
370 }
371
372 #[inline]
373 pub fn set_lite_bind_topic(&mut self, lite_bind_topic: Option<CheetahString>) {
374 let key = CheetahString::from_static_str(LITE_BIND_TOPIC_ATTRIBUTE_NAME);
375 match lite_bind_topic {
376 Some(value) if !value.is_empty() => {
377 self.attributes.insert(key, value);
378 }
379 _ => {
380 self.attributes.remove(&key);
381 }
382 }
383 }
384}
385
386#[cfg(test)]
387mod subscription_group_config_tests {
388 use super::*;
389 use crate::protocol::subscription::group_retry_policy_type::GroupRetryPolicyType;
390
391 #[test]
392 fn defaults_and_mutators_preserve_configuration() {
393 let mut config = SubscriptionGroupConfig::new("initial_group".into());
394 assert_eq!(config.group_name(), "initial_group");
395 assert_eq!(
396 (
397 config.consume_enable(),
398 config.consume_from_min_enable(),
399 config.consume_broadcast_enable(),
400 config.consume_message_orderly(),
401 config.retry_queue_nums(),
402 config.retry_max_times(),
403 config.broker_id(),
404 config.which_broker_when_consume_slowly(),
405 config.notify_consumer_ids_changed_enable(),
406 config.group_sys_flag(),
407 config.consume_timeout_minute(),
408 ),
409 (true, true, true, false, 1, 16, MASTER_ID, 1, true, 0, 15)
410 );
411 assert_eq!(config.group_retry_policy().type_(), GroupRetryPolicyType::Customized);
412 assert!(config.subscription_data_set().is_none());
413 assert!(config.attributes().is_empty());
414
415 let mut retry_policy = GroupRetryPolicy::default();
416 retry_policy.set_type_(GroupRetryPolicyType::Exponential);
417 config.set_group_name("test_group".into());
418 config.set_consume_enable(false);
419 config.set_consume_from_min_enable(true);
420 config.set_consume_broadcast_enable(false);
421 config.set_consume_message_orderly(true);
422 config.set_retry_queue_nums(2);
423 config.set_retry_max_times(10);
424 config.set_group_retry_policy(retry_policy);
425 config.set_broker_id(2);
426 config.set_which_broker_when_consume_slowly(2);
427 config.set_notify_consumer_ids_changed_enable(false);
428 config.set_group_sys_flag(1);
429 config.set_consume_timeout_minute(30);
430 config.set_subscription_data_set(Some(HashSet::from([SimpleSubscriptionData::new(
431 "topic".to_string(),
432 "TAG".to_string(),
433 "*".to_string(),
434 1,
435 )])));
436 config.set_attributes(HashMap::from([("key".into(), "value".into())]));
437
438 assert_eq!(
439 (
440 config.group_name().as_str(),
441 config.consume_enable(),
442 config.consume_from_min_enable(),
443 config.consume_broadcast_enable(),
444 config.consume_message_orderly(),
445 config.retry_queue_nums(),
446 config.retry_max_times(),
447 config.broker_id(),
448 config.which_broker_when_consume_slowly(),
449 config.notify_consumer_ids_changed_enable(),
450 config.group_sys_flag(),
451 config.consume_timeout_minute(),
452 ),
453 ("test_group", false, true, false, true, 2, 10, 2, 2, false, 1, 30)
454 );
455 assert_eq!(config.group_retry_policy().type_(), GroupRetryPolicyType::Exponential);
456 assert_eq!(config.subscription_data_set().unwrap().len(), 1);
457 assert_eq!(config.attributes(), &HashMap::from([("key".into(), "value".into())]));
458 }
459
460 #[test]
461 fn lite_bind_topic_round_trips_through_attributes() {
462 let mut config = SubscriptionGroupConfig::default();
463
464 assert!(config.lite_bind_topic().is_none());
465
466 config.set_lite_bind_topic(Some("parent-topic".into()));
467 assert_eq!(
468 config.lite_bind_topic(),
469 Some(&CheetahString::from_static_str("parent-topic"))
470 );
471
472 config.set_lite_bind_topic(None);
473 assert!(config.lite_bind_topic().is_none());
474 }
475
476 #[test]
477 fn lite_subscription_attributes_use_defaults_and_parse_values() {
478 let config = SubscriptionGroupConfig::default();
479
480 assert_eq!(
481 (
482 config.lite_sub_client_quota(),
483 config.lite_sub_exclusive(),
484 config.max_client_event_count(),
485 config.reset_offset_in_exclusive_mode(),
486 config.reset_offset_on_unsubscribe(),
487 config.priority_factor(),
488 config.lite_sub_wildcard(),
489 ),
490 (2000, false, -1, false, false, 100, false)
491 );
492
493 let mut config = config;
494 config.set_attributes(HashMap::from([
495 (
496 CheetahString::from_static_str(LITE_SUB_CLIENT_QUOTA_ATTRIBUTE_NAME),
497 CheetahString::from_static_str("128"),
498 ),
499 (
500 CheetahString::from_static_str(LITE_SUB_MODEL_ATTRIBUTE_NAME),
501 CheetahString::from_static_str("Exclusive"),
502 ),
503 (
504 CheetahString::from_static_str(LITE_SUB_CLIENT_MAX_EVENT_COUNT_ATTRIBUTE_NAME),
505 CheetahString::from_static_str("256"),
506 ),
507 (
508 CheetahString::from_static_str(LITE_SUB_RESET_OFFSET_EXCLUSIVE_ATTRIBUTE_NAME),
509 CheetahString::from_static_str("true"),
510 ),
511 (
512 CheetahString::from_static_str(LITE_SUB_RESET_OFFSET_UNSUBSCRIBE_ATTRIBUTE_NAME),
513 CheetahString::from_static_str("true"),
514 ),
515 (
516 CheetahString::from_static_str(PRIORITY_FACTOR_ATTRIBUTE_NAME),
517 CheetahString::from_static_str("25"),
518 ),
519 ]));
520
521 assert_eq!(
522 (
523 config.lite_sub_client_quota(),
524 config.lite_sub_exclusive(),
525 config.max_client_event_count(),
526 config.reset_offset_in_exclusive_mode(),
527 config.reset_offset_on_unsubscribe(),
528 config.priority_factor(),
529 ),
530 (128, true, 256, true, true, 25)
531 );
532
533 config.set_attributes(HashMap::from([
534 (
535 CheetahString::from_static_str(LITE_SUB_CLIENT_QUOTA_ATTRIBUTE_NAME),
536 CheetahString::from_static_str("invalid"),
537 ),
538 (
539 CheetahString::from_static_str(LITE_SUB_CLIENT_MAX_EVENT_COUNT_ATTRIBUTE_NAME),
540 CheetahString::from_static_str("invalid"),
541 ),
542 (
543 CheetahString::from_static_str(PRIORITY_FACTOR_ATTRIBUTE_NAME),
544 CheetahString::from_static_str("invalid"),
545 ),
546 ]));
547 assert_eq!(
548 (
549 config.lite_sub_client_quota(),
550 config.max_client_event_count(),
551 config.priority_factor(),
552 ),
553 (2000, -1, 100)
554 );
555 }
556
557 #[test]
558 fn wildcard_lite_group_uses_attribute_presence_and_can_be_disabled() {
559 let mut config = SubscriptionGroupConfig::default();
560 config.set_attributes(HashMap::from([(
561 CheetahString::from_static_str(LITE_SUB_WILDCARD_ATTRIBUTE_NAME),
562 CheetahString::from_static_str("false"),
563 )]));
564
565 assert!(config.lite_sub_wildcard());
566
567 config.set_lite_sub_wildcard(false);
568 assert!(!config.lite_sub_wildcard());
569 config.set_lite_sub_wildcard(true);
570 assert!(config.lite_sub_wildcard());
571 }
572}