Skip to main content

rocketmq_protocol/protocol/subscription/
subscription_group_config.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::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
38/// Validates a consumer group name using the Java 5.5 `TopicValidator.validateGroup` contract.
39///
40/// # Errors
41///
42/// Returns [`ProtocolContractViolation`] when the name is blank, contains an
43/// unsupported character, or exceeds [`SUBSCRIPTION_GROUP_NAME_MAX_LENGTH`].
44pub 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
59/// Validates every group before a batch mutation and rejects duplicate names.
60///
61/// # Errors
62///
63/// Returns [`ProtocolContractViolation`] when any name is invalid or when the
64/// batch contains a duplicate name.
65pub 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    /// Returns whether this group follows every active Lite topic under its bound parent topic.
252    ///
253    /// Java 5.5 models this as the presence of the `lite.sub.wildcard` attribute rather than as a
254    /// normal tag expression. Preserve that presence-based wire contract for compatibility.
255    #[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}