Skip to main content

rocketmq_protocol/protocol/static_topic/
topic_queue_mapping_detail.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;
16
17use serde::Deserialize;
18use serde::Serialize;
19
20use crate::protocol::static_topic::i32_key_map_serde;
21use crate::protocol::static_topic::logic_queue_mapping_item::LogicQueueMappingItem;
22use crate::protocol::static_topic::topic_queue_mapping_info::TopicQueueMappingInfo;
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
25pub struct TopicQueueMappingDetail {
26    #[serde(flatten)]
27    pub topic_queue_mapping_info: TopicQueueMappingInfo,
28
29    #[serde(rename = "hostedQueues")]
30    #[serde(
31        default = "default_hosted_queues",
32        deserialize_with = "i32_key_map_serde::deserialize_optional_i32_key_map"
33    )]
34    pub hosted_queues: Option<HashMap<i32 /* global id */, Vec<LogicQueueMappingItem>>>,
35}
36
37fn default_hosted_queues() -> Option<HashMap<i32, Vec<LogicQueueMappingItem>>> {
38    Some(HashMap::new())
39}
40
41impl Default for TopicQueueMappingDetail {
42    fn default() -> Self {
43        Self {
44            topic_queue_mapping_info: TopicQueueMappingInfo::default(),
45            hosted_queues: default_hosted_queues(),
46        }
47    }
48}
49
50impl TopicQueueMappingDetail {
51    pub fn get_mapping_info(
52        mapping_detail: &TopicQueueMappingDetail,
53        global_id: i32,
54    ) -> Option<&Vec<LogicQueueMappingItem>> {
55        mapping_detail.hosted_queues.as_ref()?.get(&global_id)
56    }
57
58    pub fn compute_max_offset_from_mapping(mapping_detail: &TopicQueueMappingDetail, global_id: Option<i32>) -> i64 {
59        let Some(global_id) = global_id else {
60            return -1;
61        };
62        match Self::get_mapping_info(mapping_detail, global_id) {
63            Some(mapping_items) => {
64                if mapping_items.is_empty() {
65                    return -1;
66                }
67                mapping_items
68                    .last()
69                    .map_or(-1, LogicQueueMappingItem::compute_max_static_queue_offset)
70            }
71            None => -1,
72        }
73    }
74}
75
76//impl static methods(Like java static method)
77impl TopicQueueMappingDetail {
78    pub fn build_id_map(mapping_detail: &TopicQueueMappingDetail) -> HashMap<i32, i32> {
79        let mut curr_id_map = HashMap::new();
80        let broker_name = mapping_detail.topic_queue_mapping_info.bname.as_ref();
81        if let (Some(hosted_queues), Some(broker_name)) = (&mapping_detail.hosted_queues, broker_name) {
82            for (global_id, items) in hosted_queues {
83                if let Some(last_item) = items.last() {
84                    if last_item.bname.as_ref() == Some(broker_name) {
85                        curr_id_map.insert(*global_id, last_item.queue_id);
86                    }
87                }
88            }
89        }
90        curr_id_map
91    }
92
93    pub fn clone_as_mapping_info(mapping_detail: &TopicQueueMappingDetail) -> TopicQueueMappingInfo {
94        TopicQueueMappingInfo {
95            topic: mapping_detail.topic_queue_mapping_info.topic.clone(),
96            scope: mapping_detail.topic_queue_mapping_info.scope.clone(),
97            total_queues: mapping_detail.topic_queue_mapping_info.total_queues,
98            bname: mapping_detail.topic_queue_mapping_info.bname.clone(),
99            epoch: mapping_detail.topic_queue_mapping_info.epoch,
100            dirty: mapping_detail.topic_queue_mapping_info.dirty,
101            curr_id_map: Some(Self::build_id_map(mapping_detail)),
102        }
103    }
104    pub fn put_mapping_info(
105        mapping_detail: &mut TopicQueueMappingDetail,
106        global_id: i32,
107        mapping_info: Vec<LogicQueueMappingItem>,
108    ) {
109        if mapping_info.is_empty() {
110            return;
111        }
112        mapping_detail
113            .hosted_queues
114            .get_or_insert_with(HashMap::new)
115            .insert(global_id, mapping_info);
116    }
117
118    pub fn check_if_as_physical(mapping_detail: &TopicQueueMappingDetail, global_id: i32) -> bool {
119        match Self::get_mapping_info(mapping_detail, global_id) {
120            None => true,
121            Some(mapping_items) => {
122                mapping_items.len() == 1
123                    && mapping_items
124                        .first()
125                        .is_some_and(|mapping_item| mapping_item.logic_offset == 0)
126            }
127        }
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn topic_queue_mapping_detail_default() {
137        let detail = TopicQueueMappingDetail::default();
138        assert_eq!(detail.topic_queue_mapping_info, TopicQueueMappingInfo::default());
139        assert_eq!(detail.hosted_queues, Some(HashMap::new()));
140    }
141
142    #[test]
143    fn topic_queue_mapping_detail_serde() {
144        let mut detail = TopicQueueMappingDetail::default();
145        detail.topic_queue_mapping_info.topic = Some("test".into());
146        let mut hosted_queues = HashMap::new();
147        hosted_queues.insert(1, vec![LogicQueueMappingItem::default()]);
148        detail.hosted_queues = Some(hosted_queues);
149
150        let json = serde_json::to_string(&detail).unwrap();
151        let expected = r#"{"topic":"test","scope":"__global__","totalQueues":0,"bname":null,"epoch":0,"dirty":false,"currIdMap":null,"hostedQueues":{"1":[{"gen":0,"queueId":0,"bname":null,"logicOffset":0,"startOffset":0,"endOffset":-1,"timeOfStart":-1,"timeOfEnd":-1}]}}"#;
152        assert_eq!(json, expected);
153
154        let deserialized: TopicQueueMappingDetail = serde_json::from_str(&json).unwrap();
155        assert_eq!(detail, deserialized);
156    }
157
158    #[test]
159    fn topic_queue_mapping_detail_missing_hosted_queues_uses_java_empty_default() {
160        let json = r#"{"topic":"test","scope":"__global__","totalQueues":0,"bname":null,"epoch":0,"dirty":false,"currIdMap":null}"#;
161
162        let detail: TopicQueueMappingDetail = serde_json::from_str(json).unwrap();
163
164        assert_eq!(detail.hosted_queues, Some(HashMap::new()));
165    }
166
167    #[test]
168    fn get_mapping_info() {
169        let mut detail = TopicQueueMappingDetail::default();
170        let items = vec![LogicQueueMappingItem::default()];
171        let mut hosted_queues = HashMap::new();
172        hosted_queues.insert(1, items.clone());
173        detail.hosted_queues = Some(hosted_queues);
174
175        assert_eq!(TopicQueueMappingDetail::get_mapping_info(&detail, 1).unwrap(), &items);
176        assert!(TopicQueueMappingDetail::get_mapping_info(&detail, 2).is_none());
177    }
178
179    #[test]
180    fn compute_max_offset_from_mapping() {
181        let mut detail = TopicQueueMappingDetail::default();
182        let mut items = vec![LogicQueueMappingItem::default()];
183        items[0].logic_offset = 100;
184        items[0].start_offset = 50;
185        items[0].end_offset = 150;
186        // max = logic_offset + end_offset - start_offset = 100 + 150 - 50 = 200
187
188        let mut hosted_queues = HashMap::new();
189        hosted_queues.insert(1, items);
190        detail.hosted_queues = Some(hosted_queues);
191
192        assert_eq!(
193            TopicQueueMappingDetail::compute_max_offset_from_mapping(&detail, Some(1)),
194            200
195        );
196
197        // Update end_offset to be invalid (less than start_offset)
198        detail.hosted_queues.as_mut().unwrap().get_mut(&1).unwrap()[0].end_offset = 20;
199        // max should be logic_offset = 100
200        assert_eq!(
201            TopicQueueMappingDetail::compute_max_offset_from_mapping(&detail, Some(1)),
202            100
203        );
204
205        // non-existent global_id
206        assert_eq!(
207            TopicQueueMappingDetail::compute_max_offset_from_mapping(&detail, Some(2)),
208            -1
209        );
210        assert_eq!(
211            TopicQueueMappingDetail::compute_max_offset_from_mapping(&detail, None),
212            -1
213        );
214    }
215
216    #[test]
217    fn clone_as_mapping_info() {
218        let detail = TopicQueueMappingDetail {
219            topic_queue_mapping_info: TopicQueueMappingInfo {
220                topic: Some("test_topic".into()),
221                total_queues: 8,
222                bname: Some("broker_a".into()),
223                epoch: 12345,
224                ..TopicQueueMappingInfo::default()
225            },
226            ..Default::default()
227        };
228
229        let info = TopicQueueMappingDetail::clone_as_mapping_info(&detail);
230        assert_eq!(info.topic, detail.topic_queue_mapping_info.topic);
231        assert_eq!(info.total_queues, detail.topic_queue_mapping_info.total_queues);
232        assert_eq!(info.bname, detail.topic_queue_mapping_info.bname);
233        assert_eq!(info.epoch, detail.topic_queue_mapping_info.epoch);
234    }
235
236    #[test]
237    fn clone_as_mapping_info_builds_current_id_map_for_leader_items() {
238        let broker_a = cheetah_string::CheetahString::from_static_str("broker_a");
239        let broker_b = cheetah_string::CheetahString::from_static_str("broker_b");
240        let mut hosted_queues = HashMap::new();
241        hosted_queues.insert(
242            0,
243            vec![LogicQueueMappingItem {
244                queue_id: 3,
245                bname: Some(broker_a.clone()),
246                ..Default::default()
247            }],
248        );
249        hosted_queues.insert(
250            1,
251            vec![LogicQueueMappingItem {
252                queue_id: 7,
253                bname: Some(broker_b),
254                ..Default::default()
255            }],
256        );
257        let detail = TopicQueueMappingDetail {
258            topic_queue_mapping_info: TopicQueueMappingInfo {
259                topic: Some("test_topic".into()),
260                total_queues: 2,
261                bname: Some(broker_a),
262                epoch: 10,
263                ..TopicQueueMappingInfo::default()
264            },
265            hosted_queues: Some(hosted_queues),
266        };
267
268        let info = TopicQueueMappingDetail::clone_as_mapping_info(&detail);
269
270        let curr_id_map = info.curr_id_map.unwrap();
271        assert_eq!(curr_id_map.get(&0), Some(&3));
272        assert!(!curr_id_map.contains_key(&1));
273    }
274
275    #[test]
276    fn put_mapping_info() {
277        let mut detail = TopicQueueMappingDetail::default();
278
279        let items = vec![LogicQueueMappingItem::default()];
280        TopicQueueMappingDetail::put_mapping_info(&mut detail, 1, items.clone());
281
282        assert_eq!(detail.hosted_queues.as_ref().unwrap().get(&1).unwrap(), &items);
283
284        TopicQueueMappingDetail::put_mapping_info(&mut detail, 2, vec![]);
285        assert!(detail.hosted_queues.as_ref().unwrap().get(&2).is_none());
286    }
287
288    #[test]
289    fn put_mapping_info_initializes_missing_hosted_queues_like_java() {
290        let mut detail = TopicQueueMappingDetail {
291            hosted_queues: None,
292            ..Default::default()
293        };
294
295        let items = vec![LogicQueueMappingItem::default()];
296        TopicQueueMappingDetail::put_mapping_info(&mut detail, 1, items.clone());
297
298        assert_eq!(detail.hosted_queues.as_ref().unwrap().get(&1), Some(&items));
299    }
300
301    #[test]
302    fn check_if_as_physical_matches_java_rules() {
303        let mut detail = TopicQueueMappingDetail::default();
304        assert!(TopicQueueMappingDetail::check_if_as_physical(&detail, 0));
305
306        detail.hosted_queues.as_mut().unwrap().insert(
307            0,
308            vec![LogicQueueMappingItem {
309                logic_offset: 0,
310                ..Default::default()
311            }],
312        );
313        assert!(TopicQueueMappingDetail::check_if_as_physical(&detail, 0));
314
315        detail.hosted_queues.as_mut().unwrap().insert(
316            1,
317            vec![LogicQueueMappingItem {
318                logic_offset: 10,
319                ..Default::default()
320            }],
321        );
322        assert!(!TopicQueueMappingDetail::check_if_as_physical(&detail, 1));
323
324        detail.hosted_queues.as_mut().unwrap().insert(
325            2,
326            vec![
327                LogicQueueMappingItem {
328                    logic_offset: 0,
329                    ..Default::default()
330                },
331                LogicQueueMappingItem {
332                    logic_offset: 0,
333                    ..Default::default()
334                },
335            ],
336        );
337        assert!(!TopicQueueMappingDetail::check_if_as_physical(&detail, 2));
338    }
339}