Skip to main content

mito2/sst/
file_ref.rs

1// Copyright 2023 Greptime Team
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, HashSet};
16use std::sync::Arc;
17
18use common_telemetry::debug;
19use dashmap::{DashMap, Entry};
20use store_api::storage::{FileRef, FileRefsManifest, RegionId};
21
22use crate::error::Result;
23use crate::metrics::GC_REF_FILE_CNT;
24use crate::region::MitoRegionRef;
25use crate::sst::file::FileMeta;
26
27/// File references for a region.
28/// It contains all files referenced by the region.
29#[derive(Debug, Clone, Default)]
30pub struct RegionFileRefs {
31    /// (FileRef, Ref Count) meaning how many FileHandleInner is opened for this file.
32    pub files: HashMap<FileRef, usize>,
33}
34
35/// Manages all file references in one datanode.
36/// It keeps track of which files are referenced and group by table ids.
37/// This is useful for ensuring that files are not deleted while they are still in use by any
38/// query.
39#[derive(Debug)]
40pub struct FileReferenceManager {
41    /// Datanode id. used to determine tmp ref file name.
42    node_id: Option<u64>,
43    /// TODO(discord9): use no hash hasher since table id is sequential.
44    files_per_region: DashMap<RegionId, RegionFileRefs>,
45    /// Whether global GC is enabled.
46    /// This is only meaningful when using object store (not local filesystem).
47    gc_enabled: bool,
48}
49
50pub type FileReferenceManagerRef = Arc<FileReferenceManager>;
51
52impl FileReferenceManager {
53    pub fn new(node_id: Option<u64>) -> Self {
54        Self {
55            node_id,
56            files_per_region: Default::default(),
57            gc_enabled: false,
58        }
59    }
60
61    /// Creates a new FileReferenceManager with GC configuration.
62    pub fn with_gc_enabled(node_id: Option<u64>, gc_enabled: bool) -> Self {
63        Self {
64            node_id,
65            files_per_region: Default::default(),
66            gc_enabled,
67        }
68    }
69
70    /// Returns whether global GC is enabled.
71    ///
72    /// This is useful for determining the file deletion strategy:
73    /// - If GC is enabled (and using object store), files are tracked but not immediately deleted
74    /// - If GC is disabled (or using local filesystem), files are deleted immediately
75    pub fn is_gc_enabled(&self) -> bool {
76        self.gc_enabled
77    }
78
79    fn ref_file_set(&self, region_id: RegionId) -> Option<HashSet<FileRef>> {
80        // region id not found.
81        let file_refs = self.files_per_region.get(&region_id)?.clone();
82
83        if file_refs.files.is_empty() {
84            // still return an empty manifest to indicate no files are referenced.
85            // and differentiate from error case where table_id not found.
86            return Some(HashSet::new());
87        }
88
89        let ref_file_set: HashSet<FileRef> = file_refs.files.keys().cloned().collect();
90
91        debug!(
92            "Get file refs for region {}, node {:?}, {} files",
93            region_id,
94            self.node_id,
95            ref_file_set.len(),
96        );
97
98        Some(ref_file_set)
99    }
100
101    /// Gets all ref files for the given regions, meaning all open FileHandles for those regions
102    /// and from related regions' manifests.
103    /// `query_regions_for_mem` queries for in memory file handles.
104    /// `related_regions_in_manifest` queries for related regions' manifests to get more file refs of given region ids.
105    pub(crate) async fn get_snapshot_of_file_refs(
106        &self,
107        query_regions_for_mem: Vec<MitoRegionRef>,
108        dst_region_to_src_regions: Vec<(MitoRegionRef, HashSet<RegionId>)>,
109    ) -> Result<FileRefsManifest> {
110        let mut ref_files = HashMap::new();
111        // get from in memory file handles
112        for region_id in query_regions_for_mem.iter().map(|r| r.region_id()) {
113            if let Some(files) = self.ref_file_set(region_id) {
114                ref_files.insert(region_id, files);
115            }
116        }
117
118        let mut manifest_version = HashMap::new();
119
120        let mut cross_region_refs = HashMap::new();
121
122        // get file refs from related regions' manifests
123        for (dst_region, src_regions) in &dst_region_to_src_regions {
124            let manifest = dst_region.manifest_ctx.manifest().await;
125            for meta in manifest.files.values() {
126                if src_regions.contains(&meta.region_id) {
127                    cross_region_refs
128                        .entry(meta.region_id)
129                        .or_insert_with(HashSet::new)
130                        .insert(dst_region.region_id());
131                    // since gc couldn't happen together with repartition
132                    // (both the queries and related_region acquire region read lock), no need to worry about
133                    // staging manifest in repartition here.
134                    ref_files
135                        .entry(meta.region_id)
136                        .or_insert_with(HashSet::new)
137                        .insert(FileRef::new(
138                            meta.region_id,
139                            meta.file_id,
140                            meta.index_version(),
141                        ));
142                }
143            }
144            // not sure if related region's manifest version is needed, but record it for now.
145            manifest_version.insert(dst_region.region_id(), manifest.manifest_version);
146        }
147
148        for r in &query_regions_for_mem {
149            let manifest = r.manifest_ctx.manifest().await;
150            // remove in manifest files for smaller size, since gc worker read from manifest later.
151            ref_files.entry(r.region_id()).and_modify(|refs| {
152                *refs = std::mem::take(refs)
153                    .into_iter()
154                    .filter(|f| !manifest.files.contains_key(&f.file_id))
155                    .collect();
156            });
157            manifest_version.insert(r.region_id(), manifest.manifest_version);
158        }
159
160        // simply return all ref files, no manifest version filtering for now.
161        Ok(FileRefsManifest {
162            file_refs: ref_files,
163            manifest_version,
164            cross_region_refs,
165        })
166    }
167
168    /// Adds a new file reference.
169    /// Also records the access layer for the table if not exists.
170    /// The access layer will be used to upload ref file to object storage.
171    pub fn add_file(&self, file_meta: &FileMeta) {
172        let region_id = file_meta.region_id;
173        let mut is_new = false;
174        {
175            let file_ref = FileRef::new(
176                file_meta.region_id,
177                file_meta.file_id,
178                file_meta.index_version(),
179            );
180            self.files_per_region
181                .entry(region_id)
182                .and_modify(|refs| {
183                    refs.files
184                        .entry(file_ref.clone())
185                        .and_modify(|count| *count += 1)
186                        .or_insert_with(|| {
187                            is_new = true;
188                            1
189                        });
190                })
191                .or_insert_with(|| RegionFileRefs {
192                    files: HashMap::from_iter([(file_ref, 1)]),
193                });
194        }
195        if is_new {
196            GC_REF_FILE_CNT.inc();
197        }
198    }
199
200    /// Removes a file reference.
201    /// If the reference count reaches zero, the file reference will be removed from the manager.
202    pub fn remove_file(&self, file_meta: &FileMeta) {
203        let region_id = file_meta.region_id;
204        let file_ref = FileRef::new(region_id, file_meta.file_id, file_meta.index_version());
205
206        let mut remove_table_entry = false;
207        let mut remove_file_ref = false;
208        let mut file_cnt = 0;
209
210        let region_ref = self.files_per_region.entry(region_id).and_modify(|refs| {
211            let entry = refs.files.entry(file_ref.clone()).and_modify(|count| {
212                if *count > 0 {
213                    *count -= 1;
214                }
215                if *count == 0 {
216                    remove_file_ref = true;
217                }
218            });
219            if let std::collections::hash_map::Entry::Occupied(o) = entry
220                && remove_file_ref
221            {
222                o.remove_entry();
223            }
224
225            file_cnt = refs.files.len();
226
227            if refs.files.is_empty() {
228                remove_table_entry = true;
229            }
230        });
231
232        if let Entry::Occupied(o) = region_ref
233            && remove_table_entry
234        {
235            o.remove_entry();
236        }
237        if remove_file_ref {
238            GC_REF_FILE_CNT.dec();
239        }
240    }
241
242    pub fn node_id(&self) -> Option<u64> {
243        self.node_id
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use std::num::NonZeroU64;
250
251    use smallvec::SmallVec;
252    use store_api::storage::{FileId, RegionId};
253
254    use super::*;
255    use crate::sst::file::{ColumnIndexMetadata, FileMeta, FileTimeRange, IndexType, RegionFileId};
256
257    #[tokio::test]
258    async fn test_file_ref_mgr() {
259        common_telemetry::init_default_ut_logging();
260
261        let sst_file_id = RegionFileId::new(RegionId::new(0, 0), FileId::random());
262
263        let file_ref_mgr = FileReferenceManager::new(None);
264
265        let file_meta = FileMeta {
266            region_id: sst_file_id.region_id(),
267            file_id: sst_file_id.file_id(),
268            time_range: FileTimeRange::default(),
269            level: 0,
270            file_size: 4096,
271            max_row_group_uncompressed_size: 4096,
272            available_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
273            indexes: vec![ColumnIndexMetadata {
274                column_id: 0,
275                created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
276                ..Default::default()
277            }],
278            index_file_size: 4096,
279            index_version: 0,
280            num_rows: 1024,
281            num_row_groups: 1,
282            sequence: NonZeroU64::new(4096),
283            partition_expr: None,
284            num_series: 0,
285            ..Default::default()
286        };
287
288        file_ref_mgr.add_file(&file_meta);
289
290        assert_eq!(
291            file_ref_mgr
292                .files_per_region
293                .get(&file_meta.region_id)
294                .unwrap()
295                .files,
296            HashMap::from_iter([(
297                FileRef::new(
298                    file_meta.region_id,
299                    file_meta.file_id,
300                    file_meta.index_version()
301                ),
302                1
303            )])
304        );
305
306        file_ref_mgr.add_file(&file_meta);
307
308        let expected_region_ref_manifest = HashSet::from_iter([FileRef::new(
309            file_meta.region_id,
310            file_meta.file_id,
311            file_meta.index_version(),
312        )]);
313
314        assert_eq!(
315            file_ref_mgr.ref_file_set(file_meta.region_id).unwrap(),
316            expected_region_ref_manifest
317        );
318
319        assert_eq!(
320            file_ref_mgr
321                .files_per_region
322                .get(&file_meta.region_id)
323                .unwrap()
324                .files,
325            HashMap::from_iter([(
326                FileRef::new(
327                    file_meta.region_id,
328                    file_meta.file_id,
329                    file_meta.index_version()
330                ),
331                2
332            )])
333        );
334
335        assert_eq!(
336            file_ref_mgr.ref_file_set(file_meta.region_id).unwrap(),
337            expected_region_ref_manifest
338        );
339
340        file_ref_mgr.remove_file(&file_meta);
341
342        assert_eq!(
343            file_ref_mgr
344                .files_per_region
345                .get(&file_meta.region_id)
346                .unwrap()
347                .files,
348            HashMap::from_iter([(
349                FileRef::new(
350                    file_meta.region_id,
351                    file_meta.file_id,
352                    file_meta.index_version()
353                ),
354                1
355            )])
356        );
357
358        assert_eq!(
359            file_ref_mgr.ref_file_set(file_meta.region_id).unwrap(),
360            expected_region_ref_manifest
361        );
362
363        file_ref_mgr.remove_file(&file_meta);
364
365        assert!(
366            file_ref_mgr
367                .files_per_region
368                .get(&file_meta.region_id)
369                .is_none(),
370            "{:?}",
371            file_ref_mgr.files_per_region
372        );
373
374        assert!(
375            file_ref_mgr.ref_file_set(file_meta.region_id).is_none(),
376            "{:?}",
377            file_ref_mgr.files_per_region
378        );
379    }
380}