1use std::cmp::Ordering;
16use std::collections::HashMap;
17use std::collections::HashSet;
18
19use cheetah_string::CheetahString;
20use rand::seq::SliceRandom;
21use rocketmq_model::common::config::TopicConfig;
22use rocketmq_model::common::mix_all;
23use rocketmq_model::time::current_millis;
24
25use rocketmq_error::Error;
26use rocketmq_error::Result;
27
28use crate::protocol::static_topic::logic_queue_mapping_item::LogicQueueMappingItem;
29use crate::protocol::static_topic::topic_config_and_queue_mapping::TopicConfigAndQueueMapping;
30use crate::protocol::static_topic::topic_queue_mapping_detail::TopicQueueMappingDetail;
31use crate::protocol::static_topic::topic_queue_mapping_info::TopicQueueMappingInfo;
32use crate::protocol::static_topic::topic_queue_mapping_one::TopicQueueMappingOne;
33use crate::protocol::static_topic::topic_remapping_detail_wrapper;
34use crate::protocol::static_topic::topic_remapping_detail_wrapper::TopicRemappingDetailWrapper;
35use crate::protocol::RemotingSerializable;
36
37pub struct TopicQueueMappingUtils;
38
39fn static_topic_mapping_invalid_item(reason: impl Into<String>) -> Error {
40 crate::error::invalid_argument(reason)
41}
42
43fn static_topic_mapping_inconsistent(reason: impl Into<String>) -> Error {
44 crate::error::route_inconsistent("static_topic_mapping", reason)
45}
46
47impl TopicQueueMappingUtils {
48 pub fn find_logic_queue_mapping_item(
49 mapping_items: &[LogicQueueMappingItem],
50 logic_offset: i64,
51 ignore_negative: bool,
52 ) -> Option<&LogicQueueMappingItem> {
53 if mapping_items.is_empty() {
54 return None;
55 }
56 for i in (0..mapping_items.len()).rev() {
58 let item = &mapping_items[i];
59 if ignore_negative && item.logic_offset < 0 {
60 continue;
61 }
62 if logic_offset >= item.logic_offset {
63 return Some(item);
64 }
65 }
66 for item in mapping_items.iter() {
68 if ignore_negative && item.logic_offset < 0 {
69 continue;
70 } else {
71 return Some(item);
72 }
73 }
74 None
75 }
76
77 pub fn find_next<'a>(
78 items: &'a [LogicQueueMappingItem],
79 current_item: Option<&'a LogicQueueMappingItem>,
80 ignore_negative: bool,
81 ) -> Option<&'a LogicQueueMappingItem> {
82 if items.is_empty() || current_item.is_none() {
83 return None;
84 }
85 let current_item = current_item?;
86 for i in 0..items.len() {
87 let item = &items[i];
88 if ignore_negative && item.logic_offset < 0 {
89 continue;
90 }
91 if item.gen == current_item.gen {
92 if i < items.len() - 1 {
93 let next_item = &items[i + 1];
94 if ignore_negative && next_item.logic_offset < 0 {
95 return None;
96 } else {
97 return Some(next_item);
98 }
99 } else {
100 return None;
101 }
102 }
103 }
104 None
105 }
106
107 pub fn get_mock_broker_name(scope: &str) -> CheetahString {
108 assert!(!scope.is_empty(), "Scope cannot be null");
109
110 if scope == mix_all::METADATA_SCOPE_GLOBAL {
111 format!("{}{}", mix_all::LOGICAL_QUEUE_MOCK_BROKER_PREFIX, &scope[2..]).into()
112 } else {
113 format!("{}{}", mix_all::LOGICAL_QUEUE_MOCK_BROKER_PREFIX, scope).into()
114 }
115 }
116 pub fn get_mapping_detail_from_config(
117 configs: Vec<TopicConfigAndQueueMapping>,
118 ) -> Result<Vec<TopicQueueMappingDetail>> {
119 let mut detail_list = vec![];
120 for config_mapping in &configs {
121 if let Some(detail) = &config_mapping.topic_queue_mapping_detail {
122 detail_list.push(detail.clone());
123 }
124 }
125 Ok(detail_list)
126 }
127 pub fn check_name_epoch_num_consistence(
128 topic: &CheetahString,
129 broker_config_map: &HashMap<CheetahString, TopicConfigAndQueueMapping>,
130 ) -> Result<(i64, i32)> {
131 if broker_config_map.is_empty() {
132 return Err(static_topic_mapping_inconsistent(
133 "check_name_epoch_num_consistence get empty config map",
134 ));
135 }
136 let mut max_epoch = -1;
138 let mut max_num = -1;
139 let scope = CheetahString::new();
140 for entry in broker_config_map {
141 let broker = entry.0;
142 let config_mapping = entry.1;
143 if config_mapping.topic_queue_mapping_detail.is_none() {
144 return Err(static_topic_mapping_inconsistent(format!(
145 "Mapping info should not be null in broker {}",
146 broker
147 )));
148 }
149 let mapping_detail = &config_mapping.topic_queue_mapping_detail;
150 if let Some(mapping_detail) = &mapping_detail {
151 if let Some(broker_name) = &mapping_detail.topic_queue_mapping_info.bname {
152 if !broker.eq(broker_name) {
153 return Err(static_topic_mapping_inconsistent(format!(
154 "The broker name is not equal {} != {} ",
155 broker, broker_name
156 )));
157 }
158 if mapping_detail.topic_queue_mapping_info.dirty {
159 return Err(static_topic_mapping_inconsistent(format!(
160 "The mapping info is dirty in broker {}",
161 broker
162 )));
163 }
164 if let Some(top) = &config_mapping.topic_config.topic_name {
165 if let Some(mapping_top) = &mapping_detail.topic_queue_mapping_info.topic {
166 if !top.eq(mapping_top) {
167 return Err(static_topic_mapping_inconsistent(format!(
168 "The topic name is inconsistent in broker {}",
169 broker
170 )));
171 }
172 if !topic.eq(mapping_top) {
173 return Err(static_topic_mapping_inconsistent(format!(
174 "The topic name is not match for broker {}",
175 broker
176 )));
177 }
178 if let Some(m_scope) = &mapping_detail.topic_queue_mapping_info.scope {
179 if !scope.eq(m_scope) {
180 return Err(static_topic_mapping_inconsistent(format!(
181 "scope does not match {} != {} in {}",
182 m_scope, scope, broker
183 )));
184 }
185 if max_epoch != -1 && max_epoch != mapping_detail.topic_queue_mapping_info.epoch {
186 return Err(static_topic_mapping_inconsistent(format!(
187 "epoch does not match {} != {} in {}",
188 max_epoch, mapping_detail.topic_queue_mapping_info.epoch, broker_name
189 )));
190 } else {
191 max_epoch = mapping_detail.topic_queue_mapping_info.epoch;
192 }
193 if max_num != -1 && max_num != mapping_detail.topic_queue_mapping_info.total_queues {
194 return Err(static_topic_mapping_inconsistent(format!(
195 "total queue number does not match {} != {} in {}",
196 max_num, mapping_detail.topic_queue_mapping_info.total_queues, broker_name
197 )));
198 } else {
199 max_num = mapping_detail.topic_queue_mapping_info.total_queues;
200 }
201 return Ok((max_epoch, max_num));
202 }
203 }
204 }
205 }
206 }
207 }
208 Err(static_topic_mapping_inconsistent(
209 "check_name_epoch_num_consistence err ! maybe some var is none",
210 ))
211 }
212 pub fn check_if_reuse_physical_queue(mapping_ones: &Vec<TopicQueueMappingOne>) -> Result<()> {
213 let mut physical_queue_id_map: HashMap<String, TopicQueueMappingOne> = HashMap::new();
214 for mapping_one in mapping_ones {
215 for item in mapping_one.items() {
216 if let Some(bname) = &item.bname {
217 let physical_queue_id = format!("{} - {}", bname, item.queue_id);
218 if let Some(id) = physical_queue_id_map.get(&physical_queue_id) {
219 return Err(static_topic_mapping_inconsistent(format!(
220 "Topic {} global queue id {} and {} shared the same physical queue {}",
221 mapping_one.topic(),
222 mapping_one.global_id(),
223 id.global_id(),
224 physical_queue_id
225 )));
226 }
227 physical_queue_id_map.insert(physical_queue_id, mapping_one.clone());
228 }
229 }
230 }
231 Ok(())
232 }
233
234 pub fn check_logic_queue_mapping_item_offset(items: &[LogicQueueMappingItem]) -> Result<()> {
235 if items.is_empty() {
236 return Ok(());
237 }
238 let mut last_gen = -1;
239 let mut last_offset = -1;
240 for i in (0..items.len()).rev() {
241 let item = &items[i];
242 if item.start_offset < 0 || item.gen < 0 || item.queue_id < 0 {
243 return Err(static_topic_mapping_invalid_item(
244 "The field is illegal, should not be negative",
245 ));
246 }
247 if items.len() >= 2 && i <= items.len() - 2 && items[i].logic_offset < 0 {
248 return Err(static_topic_mapping_invalid_item(
249 "The non-latest item has negative logic offset",
250 ));
251 }
252 if last_gen != -1 && item.gen >= last_gen {
253 return Err(static_topic_mapping_invalid_item(
254 "The gen does not increase monotonically",
255 ));
256 }
257
258 if item.end_offset != -1 && item.end_offset < item.start_offset {
259 return Err(static_topic_mapping_invalid_item(
260 "The endOffset is smaller than the start offset",
261 ));
262 }
263
264 if last_offset != -1 && item.logic_offset != -1 {
265 if item.logic_offset >= last_offset {
266 return Err(static_topic_mapping_invalid_item(
267 "The base logic offset does not increase monotonically",
268 ));
269 }
270 if item.compute_max_static_queue_offset() >= last_offset {
271 return Err(static_topic_mapping_invalid_item(
272 "The max logic offset does not increase monotonically",
273 ));
274 }
275 }
276 last_gen = item.gen;
277 last_offset = item.logic_offset;
278 }
279 Ok(())
280 }
281
282 pub fn check_if_leader(items: &[LogicQueueMappingItem], mapping_detail: &TopicQueueMappingDetail) -> bool {
283 let Some(item) = items.last() else {
284 return false;
285 };
286 item.bname.as_ref() == mapping_detail.topic_queue_mapping_info.bname.as_ref()
287 }
288
289 pub fn get_leader_item(items: &[LogicQueueMappingItem]) -> Result<LogicQueueMappingItem> {
290 if items.is_empty() {
291 return Err(static_topic_mapping_inconsistent(
292 "get_leader_item failed with empty items",
293 ));
294 }
295 if let Some(i) = items.last() {
296 return Ok(i.clone());
297 }
298 Err(static_topic_mapping_inconsistent(
299 "get_leader_item failed with empty items",
300 ))
301 }
302 pub fn get_leader_broker(items: &[LogicQueueMappingItem]) -> Result<CheetahString> {
303 let item = TopicQueueMappingUtils::get_leader_item(items)?;
304 if let Some(bname) = &item.bname {
305 return Ok(bname.to_string().into());
306 }
307 Err(static_topic_mapping_inconsistent(
308 "get_leader_broker fn get Item with None bname",
309 ))
310 }
311 pub fn check_and_build_mapping_items(
312 mut mapping_detail_list: Vec<TopicQueueMappingDetail>,
313 replace: bool,
314 check_consistence: bool,
315 ) -> Result<HashMap<i32, TopicQueueMappingOne>> {
316 mapping_detail_list.sort_by(|o1, o2| {
317 let sub = o1.topic_queue_mapping_info.epoch - o2.topic_queue_mapping_info.epoch;
318 if sub > 0 {
319 return std::cmp::Ordering::Greater;
320 } else if sub < 0 {
321 return std::cmp::Ordering::Less;
322 }
323 std::cmp::Ordering::Equal
324 });
325
326 let mut max_num = 0;
327 let mut global_id_map = HashMap::new();
328 for mapping_detail in &mapping_detail_list {
329 if mapping_detail.topic_queue_mapping_info.total_queues > max_num {
330 max_num = mapping_detail.topic_queue_mapping_info.total_queues;
331 }
332 if let Some(queue) = &mapping_detail.hosted_queues {
333 for entry in queue {
334 let global_id = entry.0;
335 TopicQueueMappingUtils::check_logic_queue_mapping_item_offset(entry.1)?;
336 let leader_broker_name = TopicQueueMappingUtils::get_leader_broker(entry.1)?;
337 if let Some(broker_name) = &mapping_detail.topic_queue_mapping_info.bname {
338 if !leader_broker_name.eq(broker_name) {
339 continue;
341 }
342
343 if global_id_map.contains_key(global_id) {
344 if !replace {
345 return Err(static_topic_mapping_inconsistent(format!(
346 "The queue id is duplicated in broker {} {}",
347 leader_broker_name, broker_name
348 )));
349 }
350 } else if let Some(top) = &mapping_detail.topic_queue_mapping_info.topic {
351 global_id_map.insert(
352 *global_id,
353 TopicQueueMappingOne::new(
354 mapping_detail.clone(),
355 top.clone().into(),
356 broker_name.clone().into(),
357 *global_id,
358 entry.1.clone(),
359 ),
360 );
361 }
362 }
363 }
364 }
365 }
366
367 if check_consistence {
368 if max_num as usize != global_id_map.len() {
369 return Err(static_topic_mapping_inconsistent(format!(
370 "The total queue number in config does not match the real hosted queues {} != {}",
371 max_num,
372 global_id_map.len()
373 )));
374 }
375 for i in 0..max_num {
376 if !global_id_map.contains_key(&i) {
377 return Err(static_topic_mapping_inconsistent(format!(
378 "The queue number {} is not in globalIdMap",
379 i
380 )));
381 }
382 }
383 }
384 let values = global_id_map.values().cloned().collect();
385 TopicQueueMappingUtils::check_if_reuse_physical_queue(&values)?;
386 Ok(global_id_map)
387 }
388 pub fn write_to_temp(wrapper: &TopicRemappingDetailWrapper, after: bool) -> Result<CheetahString> {
389 let topic = wrapper.topic();
390 let data = wrapper.serialize_json()?;
391 let mut suffix = topic_remapping_detail_wrapper::SUFFIX_BEFORE;
392 if after {
393 suffix = topic_remapping_detail_wrapper::SUFFIX_AFTER;
394 }
395 if let Ok(tmpdir) = std::env::var("java.io.tmpdir") {
396 let file_name = format!("{}/{}-{}{}", tmpdir, topic, wrapper.get_epoch(), suffix);
397 std::fs::write(&file_name, data)
398 .map_err(|source| crate::error::io_source("write_static_topic_mapping", source))?;
399 return Ok(file_name.into());
400 }
401
402 Err(crate::error::missing_configuration("java.io.tmpdir"))
403 }
404 pub fn check_target_brokers_complete(
405 target_brokers: &HashSet<CheetahString>,
406 broker_config_map: &HashMap<CheetahString, TopicConfigAndQueueMapping>,
407 ) -> Result<()> {
408 for broker in broker_config_map.keys() {
409 if let Some(mapping) = broker_config_map.get(broker) {
410 if let Some(detail) = mapping.get_topic_queue_mapping_detail() {
411 if let Some(queues) = &detail.hosted_queues {
412 if queues.is_empty() {
413 continue;
414 }
415 }
416 }
417 }
418 if !target_brokers.contains(broker) {
419 return Err(static_topic_mapping_inconsistent(format!(
420 "The existed broker {} does not in target brokers ",
421 broker
422 )));
423 }
424 }
425
426 Ok(())
427 }
428 pub fn check_physical_queue_consistence(
429 broker_config_map: &HashMap<CheetahString, TopicConfigAndQueueMapping>,
430 ) -> Result<()> {
431 for entry in broker_config_map {
432 let config_mapping = entry.1;
433 if let Some(detail) = config_mapping.get_topic_queue_mapping_detail() {
434 if config_mapping.topic_config.read_queue_nums < config_mapping.topic_config.write_queue_nums {
435 return Err(static_topic_mapping_inconsistent(
436 "Read queues is smaller than write queues",
437 ));
438 }
439 if let Some(queues) = &detail.hosted_queues {
440 for items in queues.values() {
441 for item in items {
442 if item.start_offset != 0 {
443 return Err(static_topic_mapping_inconsistent(
444 "The start offset does not begin from 0",
445 ));
446 }
447 if let Some(bname) = &item.bname {
448 let topic_config = broker_config_map.get(bname.as_str());
449 if topic_config.is_none() {
450 return Err(static_topic_mapping_inconsistent("The broker of item does not exist"));
451 } else if let Some(topic_config) = topic_config {
452 if item.queue_id >= topic_config.topic_config.write_queue_nums as i32 {
453 return Err(static_topic_mapping_inconsistent(
454 "The physical queue id is overflow the write queues",
455 ));
456 }
457 }
458 }
459 }
460 }
461 }
462 }
463 }
464 Ok(())
465 }
466
467 pub fn create_topic_config_mapping(
468 topic: &str,
469 queue_num: i32,
470 target_brokers: &HashSet<CheetahString>,
471 broker_config_map: &mut HashMap<CheetahString, TopicConfigAndQueueMapping>,
472 ) -> Result<TopicRemappingDetailWrapper> {
473 TopicQueueMappingUtils::check_target_brokers_complete(target_brokers, broker_config_map)?;
474 let mut global_id_map = HashMap::new();
475 let mut max_epoch_and_num = (current_millis(), queue_num);
476 if !broker_config_map.is_empty() {
477 let new_max_epoch_and_num = TopicQueueMappingUtils::check_name_epoch_num_consistence(
478 &CheetahString::from(topic),
479 broker_config_map,
480 )?;
481 max_epoch_and_num.0 = new_max_epoch_and_num.0 as u64;
482 max_epoch_and_num.1 = new_max_epoch_and_num.1;
483 let mut detail_list = vec![];
484 detail_list.extend(TopicQueueMappingUtils::get_mapping_detail_from_config(
485 broker_config_map.values().cloned().collect(),
486 )?);
487 global_id_map = TopicQueueMappingUtils::check_and_build_mapping_items(detail_list, false, true)?;
488 TopicQueueMappingUtils::check_if_reuse_physical_queue(&global_id_map.values().cloned().collect())?;
489 TopicQueueMappingUtils::check_physical_queue_consistence(broker_config_map)?;
490 }
491 if (queue_num as usize) < global_id_map.len() {
492 return Err(static_topic_mapping_inconsistent(format!(
493 "Cannot decrease the queue num for static topic {} < {}",
494 queue_num,
495 global_id_map.len()
496 )));
497 }
498 if (queue_num as usize) == global_id_map.len() {
500 return Err(static_topic_mapping_inconsistent(
501 "The topic queue num is equal the existed queue num, do nothing",
502 ));
503 }
504
505 let mut broker_num_map = HashMap::new();
507 for broker in target_brokers {
508 broker_num_map.insert(broker.clone(), 0);
509 }
510 let mut old_id_to_broker = HashMap::new();
511 for entry in &global_id_map {
512 let leader_broker = entry.1.bname();
513 old_id_to_broker.insert(*entry.0, CheetahString::from(leader_broker));
514 if !broker_num_map.contains_key(leader_broker) {
515 broker_num_map.insert(leader_broker.into(), 1);
516 } else {
517 broker_num_map.insert(leader_broker.into(), broker_num_map[leader_broker] + 1);
518 }
519 }
520 let mut allocator = MappingAllocator::new(old_id_to_broker, broker_num_map, HashMap::new());
521 allocator.up_to_num(queue_num);
522 let new_id_to_broker = allocator.id_to_broker();
523
524 let new_epoch = (max_epoch_and_num.0 + 1000).max(current_millis());
526 for e in new_id_to_broker {
527 let queue_id = e.0;
528 let broker = e.1;
529 if global_id_map.contains_key(queue_id) {
530 continue;
532 }
533 let mut config_mapping;
534 if !broker_config_map.contains_key(broker) {
535 config_mapping = TopicConfigAndQueueMapping::new(
536 TopicConfig::new(topic),
537 Some(TopicQueueMappingDetail {
538 topic_queue_mapping_info: TopicQueueMappingInfo::new(
539 topic.into(),
540 0,
541 broker.into(),
542 current_millis() as i64,
543 ),
544 hosted_queues: None,
545 }),
546 );
547 config_mapping.topic_config.write_queue_nums = 1;
548 config_mapping.topic_config.read_queue_nums = 1;
549 broker_config_map.insert(broker.clone(), config_mapping.clone());
550 } else {
551 config_mapping = broker_config_map[broker].clone();
552 config_mapping.topic_config.write_queue_nums += 1;
553 config_mapping.topic_config.read_queue_nums += 1;
554 }
555 let mapping_item = LogicQueueMappingItem {
556 gen: 0,
557 queue_id: config_mapping.topic_config.write_queue_nums as i32,
558 bname: Some(broker.clone()),
559 logic_offset: 0,
560 start_offset: 0,
561 end_offset: -1,
562 time_of_start: -1,
563 time_of_end: -1,
564 };
565 if let Some(detail) = config_mapping.topic_queue_mapping_detail.as_mut() {
566 TopicQueueMappingDetail::put_mapping_info(detail, *queue_id, vec![mapping_item]);
567 }
568 broker_config_map.insert(broker.clone(), config_mapping);
569 }
570
571 for entry in &mut *broker_config_map {
573 let config_mapping = entry.1;
574 if let Some(detail) = &mut config_mapping.topic_queue_mapping_detail {
575 detail.topic_queue_mapping_info.epoch = new_epoch as i64;
576 detail.topic_queue_mapping_info.total_queues = queue_num;
577 }
578 }
579 TopicQueueMappingUtils::check_name_epoch_num_consistence(&CheetahString::from(topic), broker_config_map)?;
582 global_id_map = TopicQueueMappingUtils::check_and_build_mapping_items(
583 TopicQueueMappingUtils::get_mapping_detail_from_config(broker_config_map.values().cloned().collect())?,
584 false,
585 true,
586 )?;
587 TopicQueueMappingUtils::check_if_reuse_physical_queue(&global_id_map.values().cloned().collect())?;
588 TopicQueueMappingUtils::check_physical_queue_consistence(broker_config_map)?;
589 let map = broker_config_map.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
590 Ok(TopicRemappingDetailWrapper::new(
591 CheetahString::from_slice(topic),
592 topic_remapping_detail_wrapper::TYPE_CREATE_OR_UPDATE.to_string().into(),
593 new_epoch,
594 map,
595 HashSet::new(),
596 HashSet::new(),
597 ))
598 }
599}
600pub struct MappingAllocator {
601 broker_num_map: HashMap<CheetahString, i32>,
602 id_to_broker: HashMap<i32, CheetahString>,
603 broker_num_map_before_remapping: HashMap<CheetahString, i32>,
605 current_index: i32,
606 least_brokers: Vec<CheetahString>,
607}
608impl MappingAllocator {
609 pub fn new(
610 id_to_broker: HashMap<i32, CheetahString>,
611 broker_num_map: HashMap<CheetahString, i32>,
612 broker_num_map_before_remapping: HashMap<CheetahString, i32>,
613 ) -> Self {
614 Self {
615 id_to_broker,
616 broker_num_map,
617 broker_num_map_before_remapping,
618 current_index: 0,
619 least_brokers: vec![],
620 }
621 }
622
623 fn fresh_state(&mut self) {
624 let mut min_num = i32::MAX;
625 for entry in &self.broker_num_map {
626 if *entry.1 < min_num {
627 self.least_brokers.clear();
628 self.least_brokers.push(entry.0.clone());
629 min_num = *entry.1;
630 } else if *entry.1 == min_num {
631 self.least_brokers.push(entry.0.clone());
632 }
633 }
634 if !self.broker_num_map_before_remapping.is_empty() {
636 self.least_brokers.sort_by(|o1, o2| {
637 let mut i1 = 0;
638 let mut i2 = 0;
639 if self.broker_num_map_before_remapping.contains_key(o1) {
640 if let Some(s) = self.broker_num_map_before_remapping.get(o1) {
641 i1 = *s;
642 }
643 }
644 if self.broker_num_map_before_remapping.contains_key(o2) {
645 if let Some(s) = self.broker_num_map_before_remapping.get(o2) {
646 i2 = *s;
647 }
648 }
649 if i1 - i2 > 0 {
650 return std::cmp::Ordering::Greater;
651 } else if i1 - i2 < 0 {
652 return Ordering::Less;
653 }
654 Ordering::Equal
655 });
656 } else {
657 let mut rng = rand::rng();
659 self.least_brokers.shuffle(&mut rng);
660 }
661 self.current_index = (self.least_brokers.len() - 1) as i32;
662 }
663 fn next_broker(&mut self) -> CheetahString {
664 if self.least_brokers.is_empty() {
665 self.fresh_state();
666 }
667 let tmp_index = self.current_index as usize % self.least_brokers.len();
668 self.least_brokers.remove(tmp_index)
669 }
670
671 pub fn broker_num_map(&self) -> &HashMap<CheetahString, i32> {
672 &self.broker_num_map
673 }
674
675 pub fn up_to_num(&mut self, max_queue_num: i32) {
676 let curr_size = self.id_to_broker.len();
677 if (max_queue_num as usize) <= curr_size {
678 return;
679 }
680 for i in curr_size..(max_queue_num as usize) {
681 let next_broker = self.next_broker();
682 if self.broker_num_map.contains_key(&next_broker) {
683 self.broker_num_map
684 .insert(next_broker.clone(), self.broker_num_map[&next_broker] + 1);
685 } else {
686 self.broker_num_map.insert(next_broker.clone(), 1);
687 }
688 self.id_to_broker.insert(i as i32, next_broker);
689 }
690 }
691
692 pub fn id_to_broker(&self) -> &HashMap<i32, CheetahString> {
693 &self.id_to_broker
694 }
695}
696
697impl TopicQueueMappingUtils {
698 pub fn check_leader_in_target_brokers(
699 mapping_ones: &[TopicQueueMappingOne],
700 target_brokers: &HashSet<CheetahString>,
701 ) -> Result<()> {
702 for mapping_one in mapping_ones {
703 if !target_brokers.contains(&CheetahString::from(mapping_one.bname())) {
704 return Err(static_topic_mapping_inconsistent(
705 "The leader broker is not in target brokers",
706 ));
707 }
708 }
709 Ok(())
710 }
711
712 pub fn block_seq_round_up(offset: i64, block_seq_size: i64) -> i64 {
713 let num = offset / block_seq_size;
714 let left = offset % block_seq_size;
715 if left < block_seq_size / 2 {
716 (num + 1) * block_seq_size
717 } else {
718 (num + 2) * block_seq_size
719 }
720 }
721
722 pub fn remapping_static_topic(
723 topic: &str,
724 broker_config_map: &mut HashMap<CheetahString, TopicConfigAndQueueMapping>,
725 target_brokers: &HashSet<CheetahString>,
726 ) -> Result<TopicRemappingDetailWrapper> {
727 let max_epoch_and_num =
728 TopicQueueMappingUtils::check_name_epoch_num_consistence(&CheetahString::from(topic), broker_config_map)?;
729
730 let mut global_id_map = TopicQueueMappingUtils::check_and_build_mapping_items(
731 TopicQueueMappingUtils::get_mapping_detail_from_config(broker_config_map.values().cloned().collect())?,
732 false,
733 true,
734 )?;
735
736 TopicQueueMappingUtils::check_physical_queue_consistence(broker_config_map)?;
737 TopicQueueMappingUtils::check_if_reuse_physical_queue(&global_id_map.values().cloned().collect())?;
738
739 let max_num = max_epoch_and_num.1;
740 let mut broker_num_map: HashMap<CheetahString, i32> = HashMap::new();
741 for broker in target_brokers {
742 broker_num_map.insert(broker.clone(), 0);
743 }
744
745 let mut broker_num_map_before_remapping: HashMap<CheetahString, i32> = HashMap::new();
746 for mapping_one in global_id_map.values() {
747 let bname = CheetahString::from(mapping_one.bname());
748 if broker_num_map_before_remapping.contains_key(&bname) {
749 broker_num_map_before_remapping.insert(bname.clone(), broker_num_map_before_remapping[&bname] + 1);
750 } else {
751 broker_num_map_before_remapping.insert(bname, 1);
752 }
753 }
754
755 let mut allocator =
756 MappingAllocator::new(HashMap::new(), broker_num_map.clone(), broker_num_map_before_remapping);
757 allocator.up_to_num(max_num);
758 let expected_broker_num_map = allocator.broker_num_map().clone();
759
760 let mut wait_assign_queues = std::collections::VecDeque::new();
761 let mut expected_id_to_broker: HashMap<i32, CheetahString> = HashMap::new();
762 let mut expected_broker_num_map_mut = expected_broker_num_map;
763 for (queue_id, mapping_one) in &global_id_map {
764 let leader_broker = CheetahString::from(mapping_one.bname());
765 if expected_broker_num_map_mut.contains_key(&leader_broker) {
766 if expected_broker_num_map_mut[&leader_broker] > 0 {
767 expected_id_to_broker.insert(*queue_id, leader_broker.clone());
768 expected_broker_num_map_mut
769 .insert(leader_broker.clone(), expected_broker_num_map_mut[&leader_broker] - 1);
770 } else {
771 wait_assign_queues.push_back(*queue_id);
772 expected_broker_num_map_mut.remove(&leader_broker);
773 }
774 } else {
775 wait_assign_queues.push_back(*queue_id);
776 }
777 }
778
779 for (broker, queue_num) in &expected_broker_num_map_mut {
780 for _ in 0..*queue_num {
781 if let Some(queue_id) = wait_assign_queues.pop_front() {
782 expected_id_to_broker.insert(queue_id, broker.clone());
783 }
784 }
785 }
786
787 let new_epoch = ((max_epoch_and_num.0 as u64 + 1000).max(current_millis())) as i64;
788
789 let mut brokers_to_map_out: HashSet<CheetahString> = HashSet::new();
791 let mut brokers_to_map_in: HashSet<CheetahString> = HashSet::new();
792
793 for (queue_id, broker) in &expected_id_to_broker {
794 let Some(topic_queue_mapping_one) = global_id_map.get(queue_id) else {
795 continue;
796 };
797 if topic_queue_mapping_one.bname() == broker.as_str() {
798 continue;
799 }
800
801 let map_in_broker = broker.clone();
803 let map_out_broker = CheetahString::from(topic_queue_mapping_one.bname());
804 brokers_to_map_in.insert(map_in_broker.clone());
805 brokers_to_map_out.insert(map_out_broker.clone());
806
807 let map_in_config = broker_config_map.entry(map_in_broker.clone()).or_insert_with(|| {
808 TopicConfigAndQueueMapping::new(
809 TopicConfig::with_queues(topic, 0, 0),
810 Some(TopicQueueMappingDetail {
811 topic_queue_mapping_info: TopicQueueMappingInfo::new(
812 topic.into(),
813 max_num,
814 map_in_broker.clone(),
815 new_epoch,
816 ),
817 hosted_queues: Some(HashMap::new()),
818 }),
819 )
820 });
821 map_in_config.topic_config.write_queue_nums += 1;
822 map_in_config.topic_config.read_queue_nums += 1;
823
824 let mut items: Vec<LogicQueueMappingItem> = topic_queue_mapping_one.items().clone();
825 let last = items.last().cloned();
826 if let Some(last) = last {
827 items.push(LogicQueueMappingItem {
828 gen: last.gen + 1,
829 queue_id: map_in_config.topic_config.write_queue_nums as i32 - 1,
830 bname: Some(map_in_broker.clone()),
831 logic_offset: -1,
832 start_offset: 0,
833 end_offset: -1,
834 time_of_start: -1,
835 time_of_end: -1,
836 });
837 }
838
839 if let Some(detail) = &mut map_in_config.topic_queue_mapping_detail {
841 if detail.hosted_queues.is_none() {
842 detail.hosted_queues = Some(HashMap::new());
843 }
844 if let Some(queues) = &mut detail.hosted_queues {
845 queues.insert(*queue_id, items.clone());
846 }
847 }
848
849 let map_out_config = broker_config_map.get_mut(&map_out_broker);
850 if let Some(map_out_config) = map_out_config {
851 if let Some(detail) = &mut map_out_config.topic_queue_mapping_detail {
852 if detail.hosted_queues.is_none() {
853 detail.hosted_queues = Some(HashMap::new());
854 }
855 if let Some(queues) = &mut detail.hosted_queues {
856 queues.insert(*queue_id, items);
857 }
858 }
859 }
860 }
861
862 for config_mapping in broker_config_map.values_mut() {
863 if let Some(detail) = &mut config_mapping.topic_queue_mapping_detail {
864 detail.topic_queue_mapping_info.epoch = new_epoch;
865 detail.topic_queue_mapping_info.total_queues = max_num;
866 }
867 }
868
869 TopicQueueMappingUtils::check_name_epoch_num_consistence(&CheetahString::from(topic), broker_config_map)?;
871 global_id_map = TopicQueueMappingUtils::check_and_build_mapping_items(
872 TopicQueueMappingUtils::get_mapping_detail_from_config(broker_config_map.values().cloned().collect())?,
873 false,
874 true,
875 )?;
876 TopicQueueMappingUtils::check_physical_queue_consistence(broker_config_map)?;
877 TopicQueueMappingUtils::check_if_reuse_physical_queue(&global_id_map.values().cloned().collect())?;
878 TopicQueueMappingUtils::check_leader_in_target_brokers(
879 &global_id_map.values().cloned().collect::<Vec<_>>(),
880 target_brokers,
881 )?;
882
883 let map = broker_config_map.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
884
885 Ok(TopicRemappingDetailWrapper::new(
886 CheetahString::from_slice(topic),
887 topic_remapping_detail_wrapper::TYPE_REMAPPING.to_string().into(),
888 new_epoch as u64,
889 map,
890 brokers_to_map_in,
891 brokers_to_map_out,
892 ))
893 }
894}
895
896#[cfg(test)]
897mod tests {
898 use std::sync::Mutex;
899
900 use rocketmq_error::fields;
901 use rocketmq_error::ViewValueRef;
902
903 use super::*;
904
905 static ENV_LOCK: Mutex<()> = Mutex::new(());
908
909 #[test]
910 fn check_logic_queue_mapping_item_offset_accepts_monotonic_items() {
911 let items = vec![
912 LogicQueueMappingItem {
913 gen: 0,
914 logic_offset: 0,
915 start_offset: 0,
916 end_offset: 9,
917 ..Default::default()
918 },
919 LogicQueueMappingItem {
920 gen: 1,
921 logic_offset: 10,
922 start_offset: 0,
923 end_offset: -1,
924 ..Default::default()
925 },
926 ];
927
928 assert!(TopicQueueMappingUtils::check_logic_queue_mapping_item_offset(&items).is_ok());
929 }
930
931 #[test]
932 fn check_logic_queue_mapping_item_offset_rejects_negative_fields_as_illegal_argument() {
933 let items = vec![LogicQueueMappingItem {
934 gen: -1,
935 logic_offset: 0,
936 start_offset: 0,
937 end_offset: -1,
938 queue_id: 0,
939 ..Default::default()
940 }];
941
942 let error = TopicQueueMappingUtils::check_logic_queue_mapping_item_offset(&items)
943 .expect_err("negative gen should be rejected");
944
945 assert_eq!(error.descriptor(), &rocketmq_error::CORE_ARGUMENT_INVALID);
946 }
947
948 #[test]
949 fn check_name_epoch_num_consistence_rejects_empty_config_as_route_inconsistent() {
950 let broker_config_map = HashMap::<CheetahString, TopicConfigAndQueueMapping>::new();
951
952 let error = TopicQueueMappingUtils::check_name_epoch_num_consistence(
953 &CheetahString::from_static_str("TopicA"),
954 &broker_config_map,
955 )
956 .expect_err("empty static topic config map should be rejected");
957
958 assert_eq!(error.descriptor(), &rocketmq_error::ROUTE_TOPIC_INCONSISTENT);
959 }
960
961 #[test]
962 fn check_if_reuse_physical_queue_reports_duplicate_physical_queue_as_route_inconsistent() {
963 let broker = CheetahString::from_static_str("broker-a");
964 let topic = CheetahString::from_static_str("TopicA");
965 let mapping_detail = TopicQueueMappingDetail {
966 topic_queue_mapping_info: TopicQueueMappingInfo::new(topic.clone(), 2, broker.clone(), 1),
967 hosted_queues: None,
968 };
969 let shared_physical_queue = LogicQueueMappingItem {
970 bname: Some(broker.clone()),
971 queue_id: 0,
972 ..Default::default()
973 };
974 let mapping_ones = vec![
975 TopicQueueMappingOne::new(
976 mapping_detail.clone(),
977 topic.to_string(),
978 broker.to_string(),
979 0,
980 vec![shared_physical_queue.clone()],
981 ),
982 TopicQueueMappingOne::new(
983 mapping_detail,
984 topic.to_string(),
985 broker.to_string(),
986 1,
987 vec![shared_physical_queue],
988 ),
989 ];
990
991 let error = TopicQueueMappingUtils::check_if_reuse_physical_queue(&mapping_ones)
992 .expect_err("duplicate physical queue should be rejected");
993
994 assert_eq!(error.descriptor(), &rocketmq_error::ROUTE_TOPIC_INCONSISTENT);
995 }
996
997 #[test]
998 fn write_to_temp_reports_missing_temp_dir_as_config_missing() {
999 struct EnvVarGuard {
1000 key: &'static str,
1001 original: Option<std::ffi::OsString>,
1002 }
1003
1004 impl EnvVarGuard {
1005 fn remove(key: &'static str) -> Self {
1006 let original = std::env::var_os(key);
1007 unsafe {
1011 std::env::remove_var(key);
1012 }
1013 Self { key, original }
1014 }
1015 }
1016
1017 impl Drop for EnvVarGuard {
1018 fn drop(&mut self) {
1019 unsafe {
1022 match &self.original {
1023 Some(value) => std::env::set_var(self.key, value),
1024 None => std::env::remove_var(self.key),
1025 }
1026 }
1027 }
1028 }
1029
1030 let _lock = ENV_LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
1031 let _guard = EnvVarGuard::remove("java.io.tmpdir");
1032
1033 let error = TopicQueueMappingUtils::write_to_temp(&TopicRemappingDetailWrapper::empty(), false)
1034 .expect_err("missing temp directory property should be rejected");
1035
1036 assert_eq!(error.descriptor(), &rocketmq_error::CORE_CONFIGURATION_MISSING);
1037 }
1038
1039 #[test]
1040 fn get_leader_broker_reports_missing_broker_as_route_inconsistent() {
1041 let items = vec![LogicQueueMappingItem {
1042 gen: 0,
1043 logic_offset: 0,
1044 start_offset: 0,
1045 end_offset: -1,
1046 queue_id: 0,
1047 bname: None,
1048 ..Default::default()
1049 }];
1050
1051 let error = TopicQueueMappingUtils::get_leader_broker(&items)
1052 .expect_err("leader item without broker should be rejected");
1053
1054 assert_eq!(error.descriptor(), &rocketmq_error::ROUTE_TOPIC_INCONSISTENT);
1055 }
1056
1057 #[test]
1058 fn check_if_leader_requires_last_item_broker_to_match_detail_broker() {
1059 let broker_a = CheetahString::from_static_str("broker-a");
1060 let broker_b = CheetahString::from_static_str("broker-b");
1061 let detail = TopicQueueMappingDetail {
1062 topic_queue_mapping_info: TopicQueueMappingInfo {
1063 bname: Some(broker_a.clone()),
1064 ..Default::default()
1065 },
1066 hosted_queues: None,
1067 };
1068 let leader_items = vec![LogicQueueMappingItem {
1069 bname: Some(broker_a),
1070 ..Default::default()
1071 }];
1072 let follower_items = vec![LogicQueueMappingItem {
1073 bname: Some(broker_b),
1074 ..Default::default()
1075 }];
1076
1077 assert!(TopicQueueMappingUtils::check_if_leader(&leader_items, &detail));
1078 assert!(!TopicQueueMappingUtils::check_if_leader(&follower_items, &detail));
1079 assert!(!TopicQueueMappingUtils::check_if_leader(&[], &detail));
1080 }
1081
1082 #[test]
1083 fn find_next_returns_none_without_current_item() {
1084 let items = vec![LogicQueueMappingItem {
1085 gen: 1,
1086 ..Default::default()
1087 }];
1088
1089 assert!(TopicQueueMappingUtils::find_next(&items, None, true).is_none());
1090 }
1091
1092 fn item(gen: i32, logic_offset: i64) -> LogicQueueMappingItem {
1093 LogicQueueMappingItem {
1094 gen,
1095 logic_offset,
1096 ..Default::default()
1097 }
1098 }
1099
1100 #[test]
1101 fn find_next_returns_following_item_and_none_after_last() {
1102 let items = vec![item(0, 0), item(1, 10), item(2, -1)];
1103
1104 assert_eq!(
1105 TopicQueueMappingUtils::find_next(&items, Some(&items[0]), true),
1106 Some(&items[1])
1107 );
1108 assert!(TopicQueueMappingUtils::find_next(&items, Some(&items[2]), true).is_none());
1109 assert!(TopicQueueMappingUtils::find_next(&items, Some(&items[1]), true).is_none());
1110 assert_eq!(
1111 TopicQueueMappingUtils::find_next(&items, Some(&items[1]), false),
1112 Some(&items[2])
1113 );
1114 }
1115
1116 #[test]
1117 fn find_logic_queue_mapping_item_scans_from_end_for_covering_item() {
1118 let items = vec![item(0, 0), item(1, 100), item(2, -1)];
1119
1120 assert_eq!(
1121 TopicQueueMappingUtils::find_logic_queue_mapping_item(&items, 150, true),
1122 Some(&items[1])
1123 );
1124 assert_eq!(
1125 TopicQueueMappingUtils::find_logic_queue_mapping_item(&items, 100, true),
1126 Some(&items[1])
1127 );
1128 assert_eq!(
1129 TopicQueueMappingUtils::find_logic_queue_mapping_item(&items, 99, true),
1130 Some(&items[0])
1131 );
1132 assert_eq!(
1133 TopicQueueMappingUtils::find_logic_queue_mapping_item(&items, 150, false),
1134 Some(&items[2])
1135 );
1136 }
1137
1138 #[test]
1139 fn find_logic_queue_mapping_item_falls_back_to_first_non_negative_item() {
1140 let items = vec![item(0, -1), item(1, 10), item(2, 20)];
1141
1142 assert_eq!(
1143 TopicQueueMappingUtils::find_logic_queue_mapping_item(&items, 5, true),
1144 Some(&items[1])
1145 );
1146 assert!(TopicQueueMappingUtils::find_logic_queue_mapping_item(&items[..1], 5, true).is_none());
1147 }
1148
1149 #[test]
1150 fn find_logic_queue_mapping_item_returns_none_for_empty_items() {
1151 assert!(TopicQueueMappingUtils::find_logic_queue_mapping_item(&[], 0, true).is_none());
1152 assert!(TopicQueueMappingUtils::find_logic_queue_mapping_item(&[], 0, false).is_none());
1153 }
1154
1155 #[test]
1156 fn get_leader_item_returns_last_item_and_rejects_empty_items() {
1157 let items = vec![item(0, 0), item(1, 10)];
1158
1159 let leader = TopicQueueMappingUtils::get_leader_item(&items).expect("non-empty items have a leader");
1160 assert_eq!(leader, items[1]);
1161
1162 let error = TopicQueueMappingUtils::get_leader_item(&[]).expect_err("empty items should be rejected");
1163 assert_eq!(error.descriptor(), &rocketmq_error::ROUTE_TOPIC_INCONSISTENT);
1164 }
1165
1166 #[test]
1167 fn check_leader_in_target_brokers_requires_every_leader_broker_in_targets() {
1168 let mapping_one = |bname: &str| {
1169 TopicQueueMappingOne::new(
1170 TopicQueueMappingDetail {
1171 topic_queue_mapping_info: TopicQueueMappingInfo::default(),
1172 hosted_queues: None,
1173 },
1174 "TopicA".to_string(),
1175 bname.to_string(),
1176 0,
1177 vec![],
1178 )
1179 };
1180 let mapping_ones = vec![mapping_one("broker-a"), mapping_one("broker-b")];
1181 let broker_a = CheetahString::from_static_str("broker-a");
1182 let broker_b = CheetahString::from_static_str("broker-b");
1183
1184 let complete = HashSet::from([broker_a.clone(), broker_b]);
1185 assert!(TopicQueueMappingUtils::check_leader_in_target_brokers(&mapping_ones, &complete).is_ok());
1186
1187 let missing_b = HashSet::from([broker_a]);
1188 let error = TopicQueueMappingUtils::check_leader_in_target_brokers(&mapping_ones, &missing_b)
1189 .expect_err("leader broker outside target brokers should be rejected");
1190 assert_eq!(error.descriptor(), &rocketmq_error::ROUTE_TOPIC_INCONSISTENT);
1191 assert_eq!(
1195 error
1196 .diagnostic_view()
1197 .expect("valid diagnostic error context")
1198 .fields()
1199 .find(|field| field.name() == fields::TOPIC.schema().name())
1200 .map(|field| field.value()),
1201 Some(ViewValueRef::Text("static_topic_mapping"))
1202 );
1203 }
1204
1205 #[test]
1206 fn get_mock_broker_name_prefixes_scope() {
1207 let prefix = mix_all::LOGICAL_QUEUE_MOCK_BROKER_PREFIX;
1208
1209 assert_eq!(
1210 TopicQueueMappingUtils::get_mock_broker_name("scope-a").as_str(),
1211 format!("{prefix}scope-a")
1212 );
1213 assert_eq!(
1214 TopicQueueMappingUtils::get_mock_broker_name(mix_all::METADATA_SCOPE_GLOBAL).as_str(),
1215 format!("{prefix}{}", &mix_all::METADATA_SCOPE_GLOBAL[2..])
1216 );
1217 }
1218
1219 #[test]
1220 #[should_panic(expected = "Scope cannot be null")]
1221 fn get_mock_broker_name_panics_on_empty_scope() {
1222 TopicQueueMappingUtils::get_mock_broker_name("");
1223 }
1224
1225 #[test]
1226 fn block_seq_round_up_rounds_by_half_block() {
1227 for (offset, expected) in [(0, 8), (1, 8), (4, 16), (8, 16), (11, 16), (12, 24)] {
1228 assert_eq!(
1229 TopicQueueMappingUtils::block_seq_round_up(offset, 8),
1230 expected,
1231 "offset {offset}"
1232 );
1233 }
1234 }
1235}