1use std::collections::HashMap;
16use std::fmt::{self, Display};
17
18use common_wal::options::WalOptions;
19use serde::{Deserialize, Serialize};
20use snafu::OptionExt;
21use store_api::storage::RegionId;
22use table::metadata::TableId;
23
24use crate::error::{Error, InvalidMetadataSnafu, Result};
25use crate::key::{MetadataKey, MetadataValue, TOPIC_REGION_PATTERN, TOPIC_REGION_PREFIX};
26use crate::kv_backend::KvBackendRef;
27use crate::kv_backend::txn::{Txn, TxnOp};
28use crate::rpc::KeyValue;
29use crate::rpc::store::{
30 BatchDeleteRequest, BatchGetRequest, BatchPutRequest, PutRequest, RangeRequest,
31};
32use crate::wal_provider::RegionWalOptions;
33
34#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37pub struct TopicRegionKey<'a> {
38 pub region_id: RegionId,
39 pub topic: &'a str,
40}
41
42#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
44pub struct TopicRegionValue {
45 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub checkpoint: Option<ReplayCheckpoint>,
47}
48
49#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord)]
50pub struct ReplayCheckpoint {
51 #[serde(default)]
52 pub entry_id: u64,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub metadata_entry_id: Option<u64>,
55}
56
57impl<'a> TopicRegionKey<'a> {
58 pub fn new(region_id: RegionId, topic: &'a str) -> Self {
59 Self { region_id, topic }
60 }
61
62 pub fn range_topic_key(topic: &str) -> String {
63 format!("{}/{}/", TOPIC_REGION_PREFIX, topic)
64 }
65}
66
67impl<'a> MetadataKey<'a, TopicRegionKey<'a>> for TopicRegionKey<'a> {
68 fn to_bytes(&self) -> Vec<u8> {
69 self.to_string().into_bytes()
70 }
71
72 fn from_bytes(bytes: &'a [u8]) -> Result<TopicRegionKey<'a>> {
73 let key = std::str::from_utf8(bytes).map_err(|e| {
74 InvalidMetadataSnafu {
75 err_msg: format!(
76 "TopicRegionKey '{}' is not a valid UTF8 string: {e}",
77 String::from_utf8_lossy(bytes)
78 ),
79 }
80 .build()
81 })?;
82 TopicRegionKey::try_from(key)
83 }
84}
85
86impl Display for TopicRegionKey<'_> {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 write!(
89 f,
90 "{}{}",
91 Self::range_topic_key(self.topic),
92 self.region_id.as_u64()
93 )
94 }
95}
96
97impl<'a> TryFrom<&'a str> for TopicRegionKey<'a> {
98 type Error = Error;
99
100 fn try_from(value: &'a str) -> Result<TopicRegionKey<'a>> {
102 let captures = TOPIC_REGION_PATTERN
103 .captures(value)
104 .context(InvalidMetadataSnafu {
105 err_msg: format!("Invalid TopicRegionKey: {}", value),
106 })?;
107 let topic = captures.get(1).map(|m| m.as_str()).unwrap();
108 let region_id = captures[2].parse::<u64>().map_err(|_| {
109 InvalidMetadataSnafu {
110 err_msg: format!("Invalid region id in TopicRegionKey: {}", value),
111 }
112 .build()
113 })?;
114 Ok(TopicRegionKey {
115 region_id: RegionId::from_u64(region_id),
116 topic,
117 })
118 }
119}
120
121impl ReplayCheckpoint {
122 pub fn new(entry_id: u64, metadata_entry_id: Option<u64>) -> Self {
124 Self {
125 entry_id,
126 metadata_entry_id,
127 }
128 }
129
130 pub fn merge_with_topic_pruned_entry_id(
132 checkpoint: Option<Self>,
133 pruned_entry_id: Option<u64>,
134 is_metric_engine: bool,
135 ) -> Option<Self> {
136 match (checkpoint, pruned_entry_id) {
137 (Some(checkpoint), Some(pruned_entry_id)) => Some(Self {
138 entry_id: checkpoint.entry_id.max(pruned_entry_id),
139 metadata_entry_id: if is_metric_engine {
140 Some(
141 checkpoint
142 .metadata_entry_id
143 .unwrap_or_default()
144 .max(pruned_entry_id),
145 )
146 } else {
147 checkpoint.metadata_entry_id
148 },
149 }),
150 (None, Some(pruned_entry_id)) => Some(Self {
151 entry_id: pruned_entry_id,
152 metadata_entry_id: is_metric_engine.then_some(pruned_entry_id),
153 }),
154 (checkpoint, None) => checkpoint,
155 }
156 }
157}
158
159impl TopicRegionValue {
160 pub fn new(checkpoint: Option<ReplayCheckpoint>) -> Self {
162 Self { checkpoint }
163 }
164
165 pub fn min_entry_id(&self) -> Option<u64> {
169 match self.checkpoint {
170 Some(ReplayCheckpoint {
171 entry_id,
172 metadata_entry_id,
173 }) => match metadata_entry_id {
174 Some(metadata_entry_id) => Some(entry_id.min(metadata_entry_id)),
175 None => Some(entry_id),
176 },
177 None => None,
178 }
179 }
180}
181
182fn topic_region_decoder(value: &KeyValue) -> Result<(TopicRegionKey<'_>, TopicRegionValue)> {
183 let key = TopicRegionKey::from_bytes(&value.key)?;
184 let value = if value.value.is_empty() {
185 TopicRegionValue::default()
186 } else {
187 TopicRegionValue::try_from_raw_value(&value.value)?
188 };
189 Ok((key, value))
190}
191
192pub struct TopicRegionManager {
194 kv_backend: KvBackendRef,
195}
196
197impl TopicRegionManager {
198 pub fn new(kv_backend: KvBackendRef) -> Self {
199 Self { kv_backend }
200 }
201
202 pub async fn put(&self, key: TopicRegionKey<'_>) -> Result<()> {
203 let put_req = PutRequest {
204 key: key.to_bytes(),
205 value: vec![],
206 prev_kv: false,
207 };
208 self.kv_backend.put(put_req).await?;
209 Ok(())
210 }
211
212 pub async fn batch_get(
213 &self,
214 keys: Vec<TopicRegionKey<'_>>,
215 ) -> Result<HashMap<RegionId, TopicRegionValue>> {
216 let raw_keys = keys.iter().map(|key| key.to_bytes()).collect::<Vec<_>>();
217 let req = BatchGetRequest { keys: raw_keys };
218 let resp = self.kv_backend.batch_get(req).await?;
219
220 let v = resp
221 .kvs
222 .into_iter()
223 .map(|kv| topic_region_decoder(&kv).map(|(key, value)| (key.region_id, value)))
224 .collect::<Result<HashMap<_, _>>>()?;
225
226 Ok(v)
227 }
228
229 pub async fn get(&self, key: TopicRegionKey<'_>) -> Result<Option<TopicRegionValue>> {
230 let key_bytes = key.to_bytes();
231 let resp = self.kv_backend.get(&key_bytes).await?;
232 let value = resp
233 .map(|kv| topic_region_decoder(&kv).map(|(_, value)| value))
234 .transpose()?;
235
236 Ok(value)
237 }
238
239 pub async fn batch_put(
240 &self,
241 keys: &[(TopicRegionKey<'_>, Option<TopicRegionValue>)],
242 ) -> Result<()> {
243 let req = BatchPutRequest {
244 kvs: keys
245 .iter()
246 .map(|(key, value)| {
247 let value = value
248 .map(|v| v.try_as_raw_value())
249 .transpose()?
250 .unwrap_or_default();
251
252 Ok(KeyValue {
253 key: key.to_bytes(),
254 value,
255 })
256 })
257 .collect::<Result<Vec<_>>>()?,
258 prev_kv: false,
259 };
260 self.kv_backend.batch_put(req).await?;
261 Ok(())
262 }
263
264 pub fn build_create_txn(
266 &self,
267 table_id: TableId,
268 region_wal_options: &RegionWalOptions,
269 ) -> Result<Txn> {
270 let topic_region_mapping = self.get_topic_region_mapping(table_id, region_wal_options);
271 let topic_region_keys = topic_region_mapping
272 .iter()
273 .map(|(region_id, topic)| TopicRegionKey::new(*region_id, topic))
274 .collect::<Vec<_>>();
275 let operations = topic_region_keys
276 .into_iter()
277 .map(|key| TxnOp::Put(key.to_bytes(), vec![]))
278 .collect::<Vec<_>>();
279 Ok(Txn::new().and_then(operations))
280 }
281
282 pub fn build_update_txn(
284 &self,
285 table_id: TableId,
286 old_region_wal_options: &RegionWalOptions,
287 new_region_wal_options: &RegionWalOptions,
288 ) -> Result<Txn> {
289 let old_mapping = self.get_topic_region_mapping(table_id, old_region_wal_options);
290 let new_mapping = self.get_topic_region_mapping(table_id, new_region_wal_options);
291
292 let old_map: HashMap<RegionId, &str> = old_mapping.into_iter().collect();
294 let new_map: HashMap<RegionId, &str> = new_mapping.into_iter().collect();
295 let mut ops = Vec::new();
296
297 for (region_id, old_topic) in &old_map {
299 match new_map.get(region_id) {
300 Some(new_topic) if *new_topic == *old_topic => {
301 }
303 _ => {
304 let key = TopicRegionKey::new(*region_id, old_topic);
306 ops.push(TxnOp::Delete(key.to_bytes()));
307 }
308 }
309 }
310
311 for (region_id, new_topic) in &new_map {
313 match old_map.get(region_id) {
314 Some(old_topic) if *old_topic == *new_topic => {
315 }
317 _ => {
318 let key = TopicRegionKey::new(*region_id, new_topic);
320 ops.push(TxnOp::Put(key.to_bytes(), vec![]));
322 }
323 }
324 }
325
326 Ok(Txn::new().and_then(ops))
327 }
328
329 pub async fn regions(&self, topic: &str) -> Result<HashMap<RegionId, TopicRegionValue>> {
331 let prefix = TopicRegionKey::range_topic_key(topic);
332 let req = RangeRequest::new().with_prefix(prefix.as_bytes());
333 let resp = self.kv_backend.range(req).await?;
334 let region_ids = resp
335 .kvs
336 .iter()
337 .map(topic_region_decoder)
338 .collect::<Result<Vec<_>>>()?;
339 Ok(region_ids
340 .into_iter()
341 .map(|(key, value)| (key.region_id, value))
342 .collect())
343 }
344
345 pub async fn delete(&self, key: TopicRegionKey<'_>) -> Result<()> {
346 let raw_key = key.to_bytes();
347 self.kv_backend.delete(&raw_key, false).await?;
348 Ok(())
349 }
350
351 pub async fn batch_delete(&self, keys: Vec<TopicRegionKey<'_>>) -> Result<()> {
352 let raw_keys = keys.iter().map(|key| key.to_bytes()).collect::<Vec<_>>();
353 let req = BatchDeleteRequest {
354 keys: raw_keys,
355 prev_kv: false,
356 };
357 self.kv_backend.batch_delete(req).await?;
358 Ok(())
359 }
360
361 pub fn get_topic_region_mapping<'a>(
367 &self,
368 table_id: TableId,
369 region_wal_options: &'a RegionWalOptions,
370 ) -> Vec<(RegionId, &'a str)> {
371 region_wal_options
372 .keys()
373 .filter_map(
374 |region_number| match region_wal_options.get(region_number) {
375 Some(WalOptions::Kafka(kafka)) => {
376 let region_id = RegionId::new(table_id, *region_number);
377 Some((region_id, kafka.topic.as_str()))
378 }
379 Some(WalOptions::RaftEngine) => None,
380 Some(WalOptions::Noop) => None,
381 Some(WalOptions::ObjectStore(_)) => None,
382 None => None,
383 },
384 )
385 .collect::<Vec<_>>()
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use std::sync::Arc;
392
393 use common_wal::options::KafkaWalOptions;
394
395 use super::*;
396 use crate::kv_backend::memory::MemoryKvBackend;
397
398 #[test]
399 fn test_merge_checkpoint_with_topic_pruned_entry_id_missing_pruned() {
400 let checkpoint = Some(ReplayCheckpoint::new(10, None));
401
402 assert_eq!(
403 ReplayCheckpoint::merge_with_topic_pruned_entry_id(checkpoint, None, true),
404 checkpoint
405 );
406 }
407
408 #[test]
409 fn test_merge_checkpoint_with_topic_pruned_entry_id_creates_checkpoint() {
410 assert_eq!(
411 ReplayCheckpoint::merge_with_topic_pruned_entry_id(None, Some(10), true),
412 Some(ReplayCheckpoint::new(10, Some(10)))
413 );
414 }
415
416 #[test]
417 fn test_merge_checkpoint_with_topic_pruned_entry_id_updates_both_ids() {
418 let checkpoint = ReplayCheckpoint::new(10, Some(5));
419
420 assert_eq!(
421 ReplayCheckpoint::merge_with_topic_pruned_entry_id(Some(checkpoint), Some(20), true),
422 Some(ReplayCheckpoint::new(20, Some(20)))
423 );
424 }
425
426 #[test]
427 fn test_merge_checkpoint_with_topic_pruned_entry_id_preserves_larger_ids() {
428 let checkpoint = ReplayCheckpoint::new(30, Some(40));
429
430 assert_eq!(
431 ReplayCheckpoint::merge_with_topic_pruned_entry_id(Some(checkpoint), Some(20), true),
432 Some(checkpoint)
433 );
434 }
435
436 #[test]
437 fn test_merge_checkpoint_with_topic_pruned_entry_id_for_mito() {
438 assert_eq!(
439 ReplayCheckpoint::merge_with_topic_pruned_entry_id(None, Some(10), false),
440 Some(ReplayCheckpoint::new(10, None))
441 );
442
443 let checkpoint = ReplayCheckpoint::new(5, Some(8));
444 assert_eq!(
445 ReplayCheckpoint::merge_with_topic_pruned_entry_id(Some(checkpoint), Some(10), false),
446 Some(ReplayCheckpoint::new(10, Some(8)))
447 );
448 }
449
450 #[tokio::test]
451 async fn test_topic_region_manager() {
452 let kv_backend = Arc::new(MemoryKvBackend::default());
453 let manager = TopicRegionManager::new(kv_backend.clone());
454
455 let topics = (0..16).map(|i| format!("topic_{}", i)).collect::<Vec<_>>();
456 let keys = (0..64)
457 .map(|i| {
458 (
459 TopicRegionKey::new(RegionId::from_u64(i), &topics[(i % 16) as usize]),
460 None,
461 )
462 })
463 .collect::<Vec<_>>();
464
465 manager.batch_put(&keys).await.unwrap();
466 let mut key_values = manager
467 .regions(&topics[0])
468 .await
469 .unwrap()
470 .into_keys()
471 .collect::<Vec<_>>();
472 let expected = keys
473 .iter()
474 .filter_map(|(key, _)| {
475 if key.topic == topics[0] {
476 Some(key.region_id)
477 } else {
478 None
479 }
480 })
481 .collect::<Vec<_>>();
482 key_values.sort_by_key(|id| id.as_u64());
483 assert_eq!(key_values, expected);
484
485 let key = TopicRegionKey::new(RegionId::from_u64(0), "topic_0");
486 manager.delete(key.clone()).await.unwrap();
487 let mut key_values = manager
488 .regions(&topics[0])
489 .await
490 .unwrap()
491 .into_keys()
492 .collect::<Vec<_>>();
493 let expected = keys
494 .iter()
495 .filter_map(|(key, _)| {
496 if key.topic == topics[0] && key.region_id != RegionId::from_u64(0) {
497 Some(key.region_id)
498 } else {
499 None
500 }
501 })
502 .collect::<Vec<_>>();
503 key_values.sort_by_key(|id| id.as_u64());
504 assert_eq!(key_values, expected);
505 }
506
507 #[test]
508 fn test_topic_region_map() {
509 let kv_backend = Arc::new(MemoryKvBackend::default());
510 let manager = TopicRegionManager::new(kv_backend.clone());
511
512 let table_id = 1;
513 let region_wal_options = (0..64)
514 .map(|i| {
515 let region_number = i;
516 let wal_options = if i % 2 == 0 {
517 WalOptions::Kafka(KafkaWalOptions::new(format!("topic_{}", i)))
518 } else {
519 WalOptions::RaftEngine
520 };
521 (region_number, wal_options)
522 })
523 .collect::<HashMap<_, _>>();
524
525 let mut topic_region_mapping =
526 manager.get_topic_region_mapping(table_id, ®ion_wal_options);
527 let mut expected = (0..64)
528 .filter_map(|i| {
529 if i % 2 == 0 {
530 Some((RegionId::new(table_id, i), format!("topic_{}", i)))
531 } else {
532 None
533 }
534 })
535 .collect::<Vec<_>>();
536 topic_region_mapping.sort_by_key(|(region_id, _)| region_id.as_u64());
537 let topic_region_map = topic_region_mapping
538 .iter()
539 .map(|(region_id, topic)| (*region_id, topic.to_string()))
540 .collect::<Vec<_>>();
541 expected.sort_by_key(|(region_id, _)| region_id.as_u64());
542 assert_eq!(topic_region_map, expected);
543 }
544
545 #[test]
546 fn test_topic_region_key_is_match() {
547 let key = "__topic_region/6f153a64-7fac-4cf6-8b0b-a7967dd73879_2/4410931412992";
548 let topic_region_key = TopicRegionKey::try_from(key).unwrap();
549 assert_eq!(
550 topic_region_key.topic,
551 "6f153a64-7fac-4cf6-8b0b-a7967dd73879_2"
552 );
553 assert_eq!(
554 topic_region_key.region_id,
555 RegionId::from_u64(4410931412992)
556 );
557 }
558
559 #[test]
560 fn test_build_create_txn() {
561 let kv_backend = Arc::new(MemoryKvBackend::default());
562 let manager = TopicRegionManager::new(kv_backend.clone());
563 let table_id = 1;
564 let region_wal_options = vec![
565 (
566 0,
567 WalOptions::Kafka(KafkaWalOptions::new("topic_0".to_string())),
568 ),
569 (
570 1,
571 WalOptions::Kafka(KafkaWalOptions::new("topic_1".to_string())),
572 ),
573 (2, WalOptions::RaftEngine), ]
575 .into_iter()
576 .collect::<HashMap<_, _>>();
577
578 let txn = manager
579 .build_create_txn(table_id, ®ion_wal_options)
580 .unwrap();
581
582 let ops = txn.req().success.clone();
585 assert_eq!(ops.len(), 2);
586
587 let keys: Vec<_> = ops
588 .iter()
589 .filter_map(|op| {
590 if let TxnOp::Put(key, _) = op {
591 TopicRegionKey::from_bytes(key).ok()
592 } else {
593 None
594 }
595 })
596 .collect();
597
598 assert_eq!(keys.len(), 2);
599 let region_ids: Vec<_> = keys.iter().map(|k| k.region_id).collect();
600 assert!(region_ids.contains(&RegionId::new(table_id, 0)));
601 assert!(region_ids.contains(&RegionId::new(table_id, 1)));
602 assert!(!region_ids.contains(&RegionId::new(table_id, 2)));
603
604 for key in keys {
606 match key.region_id.region_number() {
607 0 => assert_eq!(key.topic, "topic_0"),
608 1 => assert_eq!(key.topic, "topic_1"),
609 _ => panic!("Unexpected region number"),
610 }
611 }
612 }
613
614 #[test]
615 fn test_build_update_txn_add_new_region() {
616 let kv_backend = Arc::new(MemoryKvBackend::default());
617 let manager = TopicRegionManager::new(kv_backend.clone());
618 let table_id = 1;
619 let old_region_wal_options = vec![(
620 0,
621 WalOptions::Kafka(KafkaWalOptions::new("topic_0".to_string())),
622 )]
623 .into_iter()
624 .collect::<HashMap<_, _>>();
625 let new_region_wal_options = vec![
626 (
627 0,
628 WalOptions::Kafka(KafkaWalOptions::new("topic_0".to_string())),
629 ),
630 (
631 1,
632 WalOptions::Kafka(KafkaWalOptions::new("topic_1".to_string())),
633 ),
634 ]
635 .into_iter()
636 .collect::<HashMap<_, _>>();
637 let txn = manager
638 .build_update_txn(table_id, &old_region_wal_options, &new_region_wal_options)
639 .unwrap();
640 let ops = txn.req().success.clone();
641 assert_eq!(ops.len(), 1);
643 if let TxnOp::Put(key, _) = &ops[0] {
644 let topic_key = TopicRegionKey::from_bytes(key).unwrap();
645 assert_eq!(topic_key.region_id, RegionId::new(table_id, 1));
646 assert_eq!(topic_key.topic, "topic_1");
647 } else {
648 panic!("Expected Put operation");
649 }
650 }
651
652 #[test]
653 fn test_build_update_txn_remove_region() {
654 let kv_backend = Arc::new(MemoryKvBackend::default());
655 let manager = TopicRegionManager::new(kv_backend.clone());
656 let table_id = 1;
657 let old_region_wal_options = vec![
658 (
659 0,
660 WalOptions::Kafka(KafkaWalOptions::new("topic_0".to_string())),
661 ),
662 (
663 1,
664 WalOptions::Kafka(KafkaWalOptions::new("topic_1".to_string())),
665 ),
666 ]
667 .into_iter()
668 .collect::<HashMap<_, _>>();
669 let new_region_wal_options = vec![(
670 0,
671 WalOptions::Kafka(KafkaWalOptions::new("topic_0".to_string())),
672 )]
673 .into_iter()
674 .collect::<HashMap<_, _>>();
675 let txn = manager
676 .build_update_txn(table_id, &old_region_wal_options, &new_region_wal_options)
677 .unwrap();
678 let ops = txn.req().success.clone();
679 assert_eq!(ops.len(), 1);
681 match &ops[0] {
682 TxnOp::Delete(key) => {
683 let topic_key = TopicRegionKey::from_bytes(key).unwrap();
684 assert_eq!(topic_key.region_id, RegionId::new(table_id, 1));
685 assert_eq!(topic_key.topic, "topic_1");
686 }
687 TxnOp::Put(_, _) | TxnOp::Get(_) => {
688 panic!("Expected Delete operation");
689 }
690 }
691 }
692
693 #[test]
694 fn test_build_update_txn_change_topic() {
695 let kv_backend = Arc::new(MemoryKvBackend::default());
696 let manager = TopicRegionManager::new(kv_backend.clone());
697 let table_id = 1;
698 let old_region_wal_options = vec![(
699 0,
700 WalOptions::Kafka(KafkaWalOptions::new("topic_0".to_string())),
701 )]
702 .into_iter()
703 .collect::<HashMap<_, _>>();
704 let new_region_wal_options = vec![(
705 0,
706 WalOptions::Kafka(KafkaWalOptions::new("topic_0_new".to_string())),
707 )]
708 .into_iter()
709 .collect::<HashMap<_, _>>();
710 let txn = manager
711 .build_update_txn(table_id, &old_region_wal_options, &new_region_wal_options)
712 .unwrap();
713 let ops = txn.req().success.clone();
714 assert_eq!(ops.len(), 2);
716
717 let mut delete_found = false;
718 let mut put_found = false;
719 for op in ops {
720 match op {
721 TxnOp::Delete(key) => {
722 let topic_key = TopicRegionKey::from_bytes(&key).unwrap();
723 assert_eq!(topic_key.region_id, RegionId::new(table_id, 0));
724 assert_eq!(topic_key.topic, "topic_0");
725 delete_found = true;
726 }
727 TxnOp::Put(key, _) => {
728 let topic_key = TopicRegionKey::from_bytes(&key).unwrap();
729 assert_eq!(topic_key.region_id, RegionId::new(table_id, 0));
730 assert_eq!(topic_key.topic, "topic_0_new");
731 put_found = true;
732 }
733 TxnOp::Get(_) => {
734 panic!("Unexpected Get operation in update transaction");
736 }
737 }
738 }
739 assert!(delete_found, "Expected Delete operation for old topic");
740 assert!(put_found, "Expected Put operation for new topic");
741 }
742
743 #[test]
744 fn test_build_update_txn_no_change() {
745 let kv_backend = Arc::new(MemoryKvBackend::default());
746 let manager = TopicRegionManager::new(kv_backend.clone());
747 let table_id = 1;
748 let region_wal_options = vec![
749 (
750 0,
751 WalOptions::Kafka(KafkaWalOptions::new("topic_0".to_string())),
752 ),
753 (
754 1,
755 WalOptions::Kafka(KafkaWalOptions::new("topic_1".to_string())),
756 ),
757 ]
758 .into_iter()
759 .collect::<HashMap<_, _>>();
760 let txn = manager
761 .build_update_txn(table_id, ®ion_wal_options, ®ion_wal_options)
762 .unwrap();
763 let ops = txn.req().success.clone();
765 assert_eq!(ops.len(), 0);
766 }
767
768 #[test]
769 fn test_build_update_txn_mixed_scenarios() {
770 let kv_backend = Arc::new(MemoryKvBackend::default());
771 let manager = TopicRegionManager::new(kv_backend.clone());
772 let table_id = 1;
773 let old_region_wal_options = vec![
774 (
775 0,
776 WalOptions::Kafka(KafkaWalOptions::new("topic_0".to_string())),
777 ),
778 (
779 1,
780 WalOptions::Kafka(KafkaWalOptions::new("topic_1".to_string())),
781 ),
782 (
783 2,
784 WalOptions::Kafka(KafkaWalOptions::new("topic_2".to_string())),
785 ),
786 ]
787 .into_iter()
788 .collect::<HashMap<_, _>>();
789 let new_region_wal_options = vec![
790 (
791 0,
792 WalOptions::Kafka(KafkaWalOptions::new("topic_0".to_string())), ),
794 (
795 1,
796 WalOptions::Kafka(KafkaWalOptions::new("topic_1_new".to_string())), ),
798 (
800 3,
801 WalOptions::Kafka(KafkaWalOptions::new("topic_3".to_string())), ),
803 ]
804 .into_iter()
805 .collect::<HashMap<_, _>>();
806 let txn = manager
807 .build_update_txn(table_id, &old_region_wal_options, &new_region_wal_options)
808 .unwrap();
809
810 let ops = txn.req().success.clone();
811 assert_eq!(ops.len(), 4);
818
819 let mut delete_ops = 0;
820 let mut put_ops = 0;
821 let mut delete_region_2 = false;
822 let mut delete_region_1_old = false;
823 let mut put_region_1_new = false;
824 let mut put_region_3 = false;
825
826 for op in ops {
827 match op {
828 TxnOp::Delete(key) => {
829 delete_ops += 1;
830 let topic_key = TopicRegionKey::from_bytes(&key).unwrap();
831 match topic_key.region_id.region_number() {
832 1 => {
833 assert_eq!(topic_key.topic, "topic_1");
834 delete_region_1_old = true;
835 }
836 2 => {
837 assert_eq!(topic_key.topic, "topic_2");
838 delete_region_2 = true;
839 }
840 _ => panic!("Unexpected delete operation for region"),
841 }
842 }
843 TxnOp::Put(key, _) => {
844 put_ops += 1;
845 let topic_key: TopicRegionKey<'_> = TopicRegionKey::from_bytes(&key).unwrap();
846 match topic_key.region_id.region_number() {
847 1 => {
848 assert_eq!(topic_key.topic, "topic_1_new");
849 put_region_1_new = true;
850 }
851 3 => {
852 assert_eq!(topic_key.topic, "topic_3");
853 put_region_3 = true;
854 }
855 _ => panic!("Unexpected put operation for region"),
856 }
857 }
858 TxnOp::Get(_) => {
859 panic!("Unexpected Get operation in update transaction");
860 }
861 }
862 }
863
864 assert_eq!(delete_ops, 2);
865 assert_eq!(put_ops, 2);
866 assert!(delete_region_2, "Expected delete for removed region 2");
867 assert!(
868 delete_region_1_old,
869 "Expected delete for region 1 old topic"
870 );
871 assert!(put_region_1_new, "Expected put for region 1 new topic");
872 assert!(put_region_3, "Expected put for new region 3");
873 }
874
875 #[test]
876 fn test_build_update_txn_with_raft_engine() {
877 let kv_backend = Arc::new(MemoryKvBackend::default());
878 let manager = TopicRegionManager::new(kv_backend.clone());
879 let table_id = 1;
880 let old_region_wal_options = vec![
881 (
882 0,
883 WalOptions::Kafka(KafkaWalOptions::new("topic_0".to_string())),
884 ),
885 (1, WalOptions::RaftEngine), ]
887 .into_iter()
888 .collect::<HashMap<_, _>>();
889 let new_region_wal_options = vec![
890 (
891 0,
892 WalOptions::Kafka(KafkaWalOptions::new("topic_0".to_string())),
893 ),
894 (
895 1,
896 WalOptions::Kafka(KafkaWalOptions::new("topic_1".to_string())), ),
898 ]
899 .into_iter()
900 .collect::<HashMap<_, _>>();
901 let txn = manager
902 .build_update_txn(table_id, &old_region_wal_options, &new_region_wal_options)
903 .unwrap();
904 let ops = txn.req().success.clone();
905 assert_eq!(ops.len(), 1);
909 match &ops[0] {
910 TxnOp::Put(key, _) => {
911 let topic_key = TopicRegionKey::from_bytes(key).unwrap();
912 assert_eq!(topic_key.region_id, RegionId::new(table_id, 1));
913 assert_eq!(topic_key.topic, "topic_1");
914 }
915 TxnOp::Delete(_) | TxnOp::Get(_) => {
916 panic!("Expected Put operation for new Kafka region");
917 }
918 }
919 }
920}