meta_srv/procedure/wal_prune/
utils.rs1use std::collections::HashMap;
16use std::sync::Arc;
17use std::time::Duration;
18
19use common_meta::key::TableMetadataManagerRef;
20use common_meta::region_registry::LeaderRegionRegistryRef;
21use common_telemetry::warn;
22use itertools::{Itertools, MinMaxResult};
23use rskafka::client::Client;
24use rskafka::client::partition::{OffsetAt, PartitionClient, UnknownTopicHandling};
25use snafu::ResultExt;
26use store_api::logstore::EntryId;
27use store_api::storage::RegionId;
28
29use crate::error::{
30 BuildPartitionClientSnafu, DeleteRecordsSnafu, GetOffsetSnafu, Result,
31 TableMetadataManagerSnafu, UpdateTopicNameValueSnafu,
32};
33
34const DELETE_RECORDS_TIMEOUT: Duration = Duration::from_secs(5);
36const DEFAULT_PARTITION: i32 = 0;
38
39fn missing_region_ids(
40 all_region_ids: &[RegionId],
41 result_set: &HashMap<RegionId, u64>,
42) -> Vec<RegionId> {
43 let mut missing_region_ids = Vec::new();
44 for region_id in all_region_ids {
45 if !result_set.contains_key(region_id) {
46 missing_region_ids.push(*region_id);
47 }
48 }
49 missing_region_ids
50}
51
52pub(crate) async fn find_pruneable_entry_id_for_topic(
58 table_metadata_manager: &TableMetadataManagerRef,
59 leader_region_registry: &LeaderRegionRegistryRef,
60 topic: &str,
61) -> Result<Option<u64>> {
62 let region_ids = table_metadata_manager
63 .topic_region_manager()
64 .regions(topic)
65 .await
66 .context(TableMetadataManagerSnafu)?
67 .into_keys()
68 .collect::<Vec<_>>();
69 if region_ids.is_empty() {
70 return Ok(None);
71 }
72
73 let prunable_entry_ids_map = leader_region_registry
75 .batch_get(region_ids.iter().cloned())
76 .into_iter()
77 .map(|(region_id, region)| {
78 let prunable_entry_id = region.manifest.prunable_entry_id();
79 (region_id, prunable_entry_id)
80 })
81 .collect();
82 let missing_region_ids = missing_region_ids(®ion_ids, &prunable_entry_ids_map);
83 if !missing_region_ids.is_empty() {
84 warn!(
85 "Cannot determine prunable entry id: missing region info from heartbeat. Topic: {}, missing region ids: {:?}",
86 topic, missing_region_ids
87 );
88 return Ok(None);
89 }
90
91 let min_max_result = prunable_entry_ids_map.values().minmax();
92 match min_max_result {
93 MinMaxResult::NoElements => Ok(None),
94 MinMaxResult::OneElement(prunable_entry_id) => Ok(Some(*prunable_entry_id)),
95 MinMaxResult::MinMax(min_prunable_entry_id, _) => Ok(Some(*min_prunable_entry_id)),
96 }
97}
98
99pub(crate) fn should_trigger_prune(current: Option<u64>, prunable_entry_id: u64) -> bool {
104 match current {
105 None => true, Some(current) => prunable_entry_id > current,
107 }
108}
109
110pub(crate) async fn get_partition_client(
112 client: &Arc<Client>,
113 topic: &str,
114) -> Result<PartitionClient> {
115 client
116 .partition_client(topic, DEFAULT_PARTITION, UnknownTopicHandling::Retry)
117 .await
118 .context(BuildPartitionClientSnafu {
119 topic,
120 partition: DEFAULT_PARTITION,
121 })
122}
123
124pub(crate) async fn get_offsets_for_topic(
126 partition_client: &PartitionClient,
127 topic: &str,
128) -> Result<(u64, u64)> {
129 let earliest_offset = partition_client
130 .get_offset(OffsetAt::Earliest)
131 .await
132 .context(GetOffsetSnafu { topic })?;
133 let latest_offset = partition_client
134 .get_offset(OffsetAt::Latest)
135 .await
136 .context(GetOffsetSnafu { topic })?;
137
138 Ok((earliest_offset as u64, latest_offset as u64))
139}
140
141pub(crate) async fn update_pruned_entry_id(
143 table_metadata_manager: &TableMetadataManagerRef,
144 topic: &str,
145 pruned_entry_id: EntryId,
146) -> Result<()> {
147 let prev = table_metadata_manager
148 .topic_name_manager()
149 .get(topic)
150 .await
151 .context(TableMetadataManagerSnafu)?;
152
153 table_metadata_manager
154 .topic_name_manager()
155 .update(topic, pruned_entry_id, prev)
156 .await
157 .context(UpdateTopicNameValueSnafu { topic })?;
158
159 Ok(())
160}
161
162pub(crate) async fn delete_records(
164 partition_client: &PartitionClient,
165 topic: &str,
166 pruned_entry_id: u64,
167) -> Result<()> {
168 partition_client
169 .delete_records(
170 pruned_entry_id as i64,
175 DELETE_RECORDS_TIMEOUT.as_millis() as i32,
176 )
177 .await
178 .context(DeleteRecordsSnafu {
179 topic,
180 partition: DEFAULT_PARTITION,
181 offset: pruned_entry_id,
182 })
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn test_should_trigger_prune_none_current() {
191 assert!(should_trigger_prune(None, 10));
193 assert!(should_trigger_prune(None, 0));
194 }
195
196 #[test]
197 fn test_should_trigger_prune_prunable_greater_than_current() {
198 assert!(should_trigger_prune(Some(5), 6));
200 assert!(should_trigger_prune(Some(0), 1));
201 assert!(should_trigger_prune(Some(99), 100));
202 }
203
204 #[test]
205 fn test_should_not_trigger_prune_prunable_equal_to_current() {
206 assert!(!should_trigger_prune(Some(10), 10));
208 assert!(!should_trigger_prune(Some(0), 0));
209 }
210
211 #[test]
212 fn test_should_not_trigger_prune_prunable_less_than_current() {
213 assert!(!should_trigger_prune(Some(10), 9));
215 assert!(!should_trigger_prune(Some(100), 99));
216 }
217
218 #[test]
219 fn test_missing_region_ids_none_missing() {
220 let all_region_ids = vec![RegionId::new(1, 1), RegionId::new(2, 2)];
221 let mut result_set = HashMap::new();
222 result_set.insert(RegionId::new(1, 1), 10);
223 result_set.insert(RegionId::new(2, 2), 20);
224 let missing = missing_region_ids(&all_region_ids, &result_set);
225 assert!(missing.is_empty());
226 }
227
228 #[test]
229 fn test_missing_region_ids_some_missing() {
230 let all_region_ids = vec![
231 RegionId::new(1, 1),
232 RegionId::new(2, 2),
233 RegionId::new(3, 3),
234 ];
235 let mut result_set = HashMap::new();
236 result_set.insert(RegionId::new(1, 1), 10);
237 let missing = missing_region_ids(&all_region_ids, &result_set);
238 assert_eq!(missing, vec![RegionId::new(2, 2), RegionId::new(3, 3)]);
239 }
240
241 #[test]
242 fn test_missing_region_ids_all_missing() {
243 let all_region_ids = vec![RegionId::new(1, 1), RegionId::new(2, 2)];
244 let result_set = HashMap::new();
245 let missing = missing_region_ids(&all_region_ids, &result_set);
246 assert_eq!(missing, all_region_ids);
247 }
248
249 #[test]
250 fn test_missing_region_ids_empty_all() {
251 let all_region_ids: Vec<RegionId> = vec![];
252 let mut result_set = HashMap::new();
253 result_set.insert(RegionId::new(1, 1), 10);
254 let missing = missing_region_ids(&all_region_ids, &result_set);
255 assert!(missing.is_empty());
256 }
257}