Skip to main content

mito2/
access_layer.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::path::Path;
16use std::sync::Arc;
17use std::time::{Duration, Instant};
18
19use async_stream::try_stream;
20use common_base::readable_size::ReadableSize;
21use common_runtime::runtime::RuntimeTrait;
22use common_telemetry::warn;
23use common_time::Timestamp;
24use futures::{Stream, TryStreamExt};
25use object_store::services::Fs;
26use object_store::util::{join_dir, with_instrument_layers};
27use object_store::{ATOMIC_WRITE_DIR, ErrorKind, OLD_ATOMIC_WRITE_DIR, ObjectStore};
28use parquet::file::metadata::PageIndexPolicy;
29use smallvec::SmallVec;
30use snafu::ResultExt;
31use store_api::metadata::RegionMetadataRef;
32use store_api::region_request::PathType;
33use store_api::sst_entry::StorageSstEntry;
34use store_api::storage::{FileId, RegionId, SequenceNumber};
35
36use crate::cache::file_cache::{FileCacheRef, FileType, IndexKey};
37use crate::cache::write_cache::SstUploadRequest;
38use crate::cache::{CacheManagerRef, SstMetaPreparation, prepare_sst_meta_sync};
39use crate::config::{BloomFilterConfig, FulltextIndexConfig, IndexConfig, InvertedIndexConfig};
40use crate::error::{
41    CleanDirSnafu, DeleteIndexSnafu, DeleteIndexesSnafu, DeleteSstsSnafu, OpenDalSnafu, Result,
42};
43use crate::metrics::{COMPACTION_STAGE_ELAPSED, FLUSH_ELAPSED};
44use crate::read::FlatSource;
45use crate::region::options::IndexOptions;
46use crate::sst::file::{FileHandle, RegionFileId, RegionIndexId};
47use crate::sst::index::intermediate::IntermediateManager;
48use crate::sst::index::puffin_manager::{PuffinManagerFactory, SstPuffinManager};
49use crate::sst::index::{IndexerBuilderImpl, build_index_from_parquet_bytes};
50use crate::sst::location::{self, region_dir_from_table_dir};
51use crate::sst::parquet::reader::ParquetReaderBuilder;
52use crate::sst::parquet::writer::ParquetWriter;
53use crate::sst::parquet::{SstInfo, WriteOptions};
54use crate::sst::{DEFAULT_WRITE_CONCURRENCY, FormatType};
55
56pub type AccessLayerRef = Arc<AccessLayer>;
57/// SST write results.
58pub type SstInfoArray = SmallVec<[SstInfo; 2]>;
59
60/// Write operation type.
61#[derive(Eq, PartialEq, Debug)]
62pub enum WriteType {
63    /// Writes from flush
64    Flush,
65    /// Writes from compaction.
66    Compaction,
67}
68
69#[derive(Debug)]
70pub struct Metrics {
71    pub(crate) write_type: WriteType,
72    pub(crate) iter_source: Duration,
73    pub(crate) write_batch: Duration,
74    pub(crate) update_index: Duration,
75    pub(crate) upload_parquet: Duration,
76    pub(crate) upload_puffin: Duration,
77    pub(crate) compact_memtable: Duration,
78}
79
80impl Metrics {
81    pub fn new(write_type: WriteType) -> Self {
82        Self {
83            write_type,
84            iter_source: Default::default(),
85            write_batch: Default::default(),
86            update_index: Default::default(),
87            upload_parquet: Default::default(),
88            upload_puffin: Default::default(),
89            compact_memtable: Default::default(),
90        }
91    }
92
93    pub(crate) fn merge(mut self, other: Self) -> Self {
94        assert_eq!(self.write_type, other.write_type);
95        self.iter_source += other.iter_source;
96        self.write_batch += other.write_batch;
97        self.update_index += other.update_index;
98        self.upload_parquet += other.upload_parquet;
99        self.upload_puffin += other.upload_puffin;
100        self.compact_memtable += other.compact_memtable;
101        self
102    }
103
104    pub(crate) fn observe(self) {
105        match self.write_type {
106            WriteType::Flush => {
107                FLUSH_ELAPSED
108                    .with_label_values(&["iter_source"])
109                    .observe(self.iter_source.as_secs_f64());
110                FLUSH_ELAPSED
111                    .with_label_values(&["write_batch"])
112                    .observe(self.write_batch.as_secs_f64());
113                FLUSH_ELAPSED
114                    .with_label_values(&["update_index"])
115                    .observe(self.update_index.as_secs_f64());
116                FLUSH_ELAPSED
117                    .with_label_values(&["upload_parquet"])
118                    .observe(self.upload_parquet.as_secs_f64());
119                FLUSH_ELAPSED
120                    .with_label_values(&["upload_puffin"])
121                    .observe(self.upload_puffin.as_secs_f64());
122                if !self.compact_memtable.is_zero() {
123                    FLUSH_ELAPSED
124                        .with_label_values(&["compact_memtable"])
125                        .observe(self.upload_puffin.as_secs_f64());
126                }
127            }
128            WriteType::Compaction => {
129                COMPACTION_STAGE_ELAPSED
130                    .with_label_values(&["iter_source"])
131                    .observe(self.iter_source.as_secs_f64());
132                COMPACTION_STAGE_ELAPSED
133                    .with_label_values(&["write_batch"])
134                    .observe(self.write_batch.as_secs_f64());
135                COMPACTION_STAGE_ELAPSED
136                    .with_label_values(&["update_index"])
137                    .observe(self.update_index.as_secs_f64());
138                COMPACTION_STAGE_ELAPSED
139                    .with_label_values(&["upload_parquet"])
140                    .observe(self.upload_parquet.as_secs_f64());
141                COMPACTION_STAGE_ELAPSED
142                    .with_label_values(&["upload_puffin"])
143                    .observe(self.upload_puffin.as_secs_f64());
144            }
145        };
146    }
147}
148
149/// A layer to access SST files under the same directory.
150pub struct AccessLayer {
151    table_dir: String,
152    /// Path type for generating file paths.
153    path_type: PathType,
154    /// Target object store.
155    object_store: ObjectStore,
156    /// Puffin manager factory for index.
157    puffin_manager_factory: PuffinManagerFactory,
158    /// Intermediate manager for inverted index.
159    intermediate_manager: IntermediateManager,
160}
161
162impl std::fmt::Debug for AccessLayer {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        f.debug_struct("AccessLayer")
165            .field("table_dir", &self.table_dir)
166            .finish()
167    }
168}
169
170impl AccessLayer {
171    /// Returns a new [AccessLayer] for specific `table_dir`.
172    pub fn new(
173        table_dir: impl Into<String>,
174        path_type: PathType,
175        object_store: ObjectStore,
176        puffin_manager_factory: PuffinManagerFactory,
177        intermediate_manager: IntermediateManager,
178    ) -> AccessLayer {
179        AccessLayer {
180            table_dir: table_dir.into(),
181            path_type,
182            object_store,
183            puffin_manager_factory,
184            intermediate_manager,
185        }
186    }
187
188    /// Returns the directory of the table.
189    pub fn table_dir(&self) -> &str {
190        &self.table_dir
191    }
192
193    /// Returns the object store of the layer.
194    pub fn object_store(&self) -> &ObjectStore {
195        &self.object_store
196    }
197
198    /// Returns the path type of the layer.
199    pub fn path_type(&self) -> PathType {
200        self.path_type
201    }
202
203    /// Returns the puffin manager factory.
204    pub fn puffin_manager_factory(&self) -> &PuffinManagerFactory {
205        &self.puffin_manager_factory
206    }
207
208    /// Returns the intermediate manager.
209    pub fn intermediate_manager(&self) -> &IntermediateManager {
210        &self.intermediate_manager
211    }
212
213    /// Build the puffin manager.
214    pub(crate) fn build_puffin_manager(&self) -> SstPuffinManager {
215        let store = self.object_store.clone();
216        let path_provider =
217            RegionFilePathFactory::new(self.table_dir().to_string(), self.path_type());
218        self.puffin_manager_factory.build(store, path_provider)
219    }
220
221    pub(crate) async fn delete_index(
222        &self,
223        index_file_id: RegionIndexId,
224    ) -> Result<(), crate::error::Error> {
225        let path = location::index_file_path(
226            &self.table_dir,
227            RegionIndexId::new(index_file_id.file_id, index_file_id.version),
228            self.path_type,
229        );
230        self.object_store
231            .delete(&path)
232            .await
233            .context(DeleteIndexSnafu {
234                file_id: index_file_id.file_id(),
235            })?;
236        Ok(())
237    }
238
239    pub(crate) async fn delete_ssts(
240        &self,
241        region_id: RegionId,
242        file_ids: &[FileId],
243    ) -> Result<(), crate::error::Error> {
244        if file_ids.is_empty() {
245            return Ok(());
246        }
247
248        let attempted_files = file_ids.to_vec();
249        // Deleter does not normalize leading slashes like Operator::delete does.
250        let paths: Vec<_> = file_ids
251            .iter()
252            .map(|file_id| {
253                location::sst_file_path(
254                    &self.table_dir,
255                    RegionFileId::new(region_id, *file_id),
256                    self.path_type,
257                )
258                .trim_start_matches('/')
259                .to_string()
260            })
261            .collect();
262
263        let mut deleter = self
264            .object_store
265            .deleter()
266            .await
267            .with_context(|_| DeleteSstsSnafu {
268                region_id,
269                file_ids: attempted_files.clone(),
270            })?;
271        deleter
272            .delete_iter(paths)
273            .await
274            .with_context(|_| DeleteSstsSnafu {
275                region_id,
276                file_ids: attempted_files.clone(),
277            })?;
278        deleter.close().await.with_context(|_| DeleteSstsSnafu {
279            region_id,
280            file_ids: attempted_files,
281        })?;
282
283        Ok(())
284    }
285
286    pub(crate) async fn delete_indexes(
287        &self,
288        index_ids: &[RegionIndexId],
289    ) -> Result<(), crate::error::Error> {
290        if index_ids.is_empty() {
291            return Ok(());
292        }
293
294        let file_ids: Vec<_> = index_ids
295            .iter()
296            .map(|index_id| index_id.file_id())
297            .collect();
298        let paths: Vec<_> = index_ids
299            .iter()
300            .map(|index_id| {
301                location::index_file_path(&self.table_dir, *index_id, self.path_type)
302                    .trim_start_matches('/')
303                    .to_string()
304            })
305            .collect();
306
307        let mut deleter = self
308            .object_store
309            .deleter()
310            .await
311            .context(DeleteIndexesSnafu {
312                file_ids: file_ids.clone(),
313            })?;
314        deleter
315            .delete_iter(paths)
316            .await
317            .context(DeleteIndexesSnafu {
318                file_ids: file_ids.clone(),
319            })?;
320        deleter
321            .close()
322            .await
323            .context(DeleteIndexesSnafu { file_ids })?;
324
325        Ok(())
326    }
327
328    /// Returns the directory of the region in the table.
329    pub fn build_region_dir(&self, region_id: RegionId) -> String {
330        region_dir_from_table_dir(&self.table_dir, region_id, self.path_type)
331    }
332
333    /// Returns a reader builder for specific `file`.
334    pub(crate) fn read_sst(&self, file: FileHandle) -> ParquetReaderBuilder {
335        ParquetReaderBuilder::new(
336            self.table_dir.clone(),
337            self.path_type,
338            file,
339            self.object_store.clone(),
340        )
341    }
342
343    /// Writes a SST with specific `file_id` and `metadata` to the layer.
344    ///
345    /// Returns the info of the SST. If no data written, returns None.
346    pub async fn write_sst(
347        &self,
348        request: SstWriteRequest,
349        write_opts: &WriteOptions,
350        metrics: &mut Metrics,
351    ) -> Result<SstInfoArray> {
352        self.write_sst_inner(request, write_opts, metrics, None)
353            .await
354    }
355
356    /// Writes an SST whose output is registered with `context.guard`.
357    ///
358    /// The write cache is bypassed, upload concurrency is fixed to one, and
359    /// the guard runs before either the SST or its index becomes visible.
360    /// `context.limits` bounds the encoder and footer workspace.
361    #[cfg(feature = "enterprise")]
362    pub(crate) async fn write_sst_with_guard(
363        &self,
364        request: SstWriteRequest,
365        write_opts: &WriteOptions,
366        metrics: &mut Metrics,
367        context: crate::sst::parquet::writer::SstWriteContext,
368    ) -> Result<SstInfoArray> {
369        self.write_sst_inner(request, write_opts, metrics, Some(context))
370            .await
371    }
372
373    async fn write_sst_inner(
374        &self,
375        request: SstWriteRequest,
376        write_opts: &WriteOptions,
377        metrics: &mut Metrics,
378        context: Option<crate::sst::parquet::writer::SstWriteContext>,
379    ) -> Result<SstInfoArray> {
380        let limits = context.as_ref().map(|context| context.limits);
381        let guard = context.map(|context| context.guard);
382        let op_type = request.op_type;
383        let region_id = request.metadata.region_id;
384        let region_metadata = request.metadata.clone();
385        let cache_manager = request.cache_manager.clone();
386        let override_sequence = if request.preserve_row_sequence {
387            None
388        } else {
389            request.max_sequence
390        };
391
392        let sst_info = if let Some(write_cache) = cache_manager.write_cache()
393            && guard.is_none()
394        {
395            // Write to the write cache.
396            write_cache
397                .write_and_upload_sst(
398                    request,
399                    SstUploadRequest {
400                        dest_path_provider: RegionFilePathFactory::new(
401                            self.table_dir.clone(),
402                            self.path_type,
403                        ),
404                        remote_store: self.object_store.clone(),
405                    },
406                    write_opts,
407                    metrics,
408                )
409                .await?
410        } else {
411            // Write cache is disabled, or bypassed by an SST write guard.
412            let store = self.object_store.clone();
413            let path_provider = RegionFilePathFactory::new(self.table_dir.clone(), self.path_type);
414            let puffin_factory = if limits.is_some() {
415                self.puffin_manager_factory
416                    .clone()
417                    .with_write_buffer_size(write_opts.write_buffer_size.as_bytes() as usize)
418            } else {
419                self.puffin_manager_factory.clone()
420            };
421            let indexer_builder = IndexerBuilderImpl {
422                build_type: request.op_type.into(),
423                metadata: request.metadata.clone(),
424                puffin_manager: puffin_factory.build(store, path_provider.clone()),
425                write_cache_enabled: false,
426                intermediate_manager: self.intermediate_manager.clone(),
427                index_options: request.index_options,
428                inverted_index_config: request.inverted_index_config,
429                fulltext_index_config: request.fulltext_index_config,
430                bloom_filter_index_config: request.bloom_filter_index_config,
431            };
432            // We disable write cache on file system but we still use atomic write.
433            // TODO(yingwen): If we support other non-fs stores without the write cache, then
434            // we may have find a way to check whether we need the cleaner.
435            let cleaner = TempFileCleaner::new(region_id, self.object_store.clone());
436            let mut writer = ParquetWriter::new_with_object_store(
437                self.object_store.clone(),
438                request.metadata,
439                request.index_config,
440                indexer_builder,
441                path_provider,
442                metrics,
443            )
444            .await
445            .with_file_cleaner(cleaner)
446            .with_write_concurrency(if guard.is_some() {
447                1
448            } else {
449                crate::sst::DEFAULT_WRITE_CONCURRENCY
450            })
451            .with_write_guard(guard)
452            .with_memory_limits(limits);
453            match request.sst_write_format {
454                FormatType::PrimaryKey => {
455                    writer
456                        .write_all_flat_as_primary_key(
457                            request.source,
458                            override_sequence,
459                            write_opts,
460                        )
461                        .await?
462                }
463                FormatType::Flat => {
464                    writer
465                        .write_all_flat(request.source, override_sequence, write_opts)
466                        .await?
467                }
468            }
469        };
470
471        // Put parquet metadata to cache manager.
472        if !sst_info.is_empty() && cache_manager.sst_meta_cache_on_write_enabled() {
473            let runtime = match op_type {
474                OperationType::Compact => common_runtime::compact_runtime(),
475                OperationType::Flush => common_runtime::global_runtime(),
476            };
477            for sst in &sst_info {
478                if let Some(parquet_metadata) = &sst.file_metadata {
479                    let file_id = RegionFileId::new(region_id, sst.file_id);
480                    let file_path = format!(
481                        "region_id={}, file_id={}",
482                        file_id.region_id(),
483                        file_id.file_id()
484                    );
485                    let page_index_policy = if parquet_metadata.offset_index().is_some() {
486                        PageIndexPolicy::Optional
487                    } else {
488                        PageIndexPolicy::Skip
489                    };
490                    let parquet_metadata = parquet_metadata.clone();
491                    let region_metadata = region_metadata.clone();
492                    let cache_manager = cache_manager.clone();
493                    // Prepare and cache metadata in the background without delaying the SST write.
494                    let task_runtime = runtime.clone();
495                    runtime.spawn(async move {
496                        let preparation = task_runtime.spawn_blocking(move || {
497                            prepare_sst_meta_sync(
498                                &file_path,
499                                Arc::unwrap_or_clone(parquet_metadata),
500                                Some(region_metadata),
501                                page_index_policy,
502                            )
503                        }).await.context(crate::error::JoinSnafu).and_then(|result| result);
504                        match preparation {
505                            Ok(SstMetaPreparation::Prepared(metadata)) => {
506                                cache_manager.put_compact_sst_meta(file_id, metadata.compact);
507                                cache_manager.put_sst_meta_data(file_id, metadata.decoded).await;
508                            }
509                            Ok(SstMetaPreparation::DecodedOnly { encoding_error, .. }) => warn!(
510                                encoding_error;
511                                "Failed to encode parquet metadata for cache, file: {}",
512                                file_id.file_id()
513                            ),
514                            Err(err) => {
515                                warn!(err; "Failed to cache parquet metadata for {}", file_id.file_id());
516                            }
517                        }
518                    });
519                }
520            }
521        }
522
523        Ok(sst_info)
524    }
525
526    /// Puts encoded SST bytes to the write cache (if enabled) and uploads it to the object store.
527    ///
528    /// Builds the SST's indexes from `data` into `sst_info.index_metadata` when
529    /// `index_build_params` is set.
530    pub(crate) async fn put_sst(
531        &self,
532        data: &bytes::Bytes,
533        region_id: RegionId,
534        sst_info: &mut SstInfo,
535        cache_manager: &CacheManagerRef,
536        write_buffer_size: ReadableSize,
537        index_build_params: Option<EncodedSstIndexBuildParams>,
538    ) -> Result<Metrics> {
539        let region_file_id = RegionFileId::new(region_id, sst_info.file_id);
540        if let Some(write_cache) = cache_manager.write_cache() {
541            // Write to cache and upload to remote store
542            let upload_request = SstUploadRequest {
543                dest_path_provider: RegionFilePathFactory::new(
544                    self.table_dir.clone(),
545                    self.path_type,
546                ),
547                remote_store: self.object_store.clone(),
548            };
549            write_cache
550                .put_and_upload_sst(
551                    data,
552                    region_id,
553                    sst_info,
554                    upload_request,
555                    write_buffer_size,
556                    index_build_params,
557                )
558                .await
559        } else {
560            let start = Instant::now();
561            let cleaner =
562                TempFileCleaner::new(region_file_id.region_id(), self.object_store.clone());
563            let path_provider = RegionFilePathFactory::new(self.table_dir.clone(), self.path_type);
564            let sst_file_path = path_provider.build_sst_file_path(region_file_id);
565            let mut writer = self
566                .object_store
567                .writer_with(&sst_file_path)
568                .chunk(write_buffer_size.as_bytes() as usize)
569                .concurrent(DEFAULT_WRITE_CONCURRENCY)
570                .await
571                .context(OpenDalSnafu)?;
572            if let Err(err) = writer.write(data.clone()).await.context(OpenDalSnafu) {
573                cleaner.clean_by_file_id(region_file_id.file_id()).await;
574                return Err(err);
575            }
576            if let Err(err) = writer.close().await.context(OpenDalSnafu) {
577                cleaner.clean_by_file_id(region_file_id.file_id()).await;
578                return Err(err);
579            }
580            let mut metrics = Metrics::new(WriteType::Flush);
581            metrics.write_batch = start.elapsed();
582
583            if let Some(params) = index_build_params {
584                let start = Instant::now();
585                let builder = params.indexer_builder(
586                    self.puffin_manager_factory
587                        .build(self.object_store.clone(), path_provider),
588                    false,
589                    self.intermediate_manager.clone(),
590                );
591                sst_info.index_metadata =
592                    build_index_from_parquet_bytes(&builder, region_file_id, data).await;
593                metrics.update_index = start.elapsed();
594            }
595            Ok(metrics)
596        }
597    }
598
599    /// Lists the SST entries from the storage layer in the table directory.
600    pub fn storage_sst_entries(&self) -> impl Stream<Item = Result<StorageSstEntry>> + use<> {
601        let object_store = self.object_store.clone();
602        let table_dir = self.table_dir.clone();
603
604        try_stream! {
605            let mut lister = object_store
606                .lister_with(table_dir.as_str())
607                .recursive(true)
608                .await
609                .context(OpenDalSnafu)?;
610
611            while let Some(entry) = lister.try_next().await.context(OpenDalSnafu)? {
612                let metadata = entry.metadata();
613                if metadata.is_dir() {
614                    continue;
615                }
616
617                let path = entry.path();
618                if !path.ends_with(".parquet") && !path.ends_with(".puffin") {
619                    continue;
620                }
621
622                let file_size = metadata.content_length();
623                let file_size = if file_size == 0 { None } else { Some(file_size) };
624                let last_modified_ms = metadata
625                    .last_modified()
626                    .map(|ts| Timestamp::new_millisecond(ts.into_inner().as_millisecond()));
627
628                let entry = StorageSstEntry {
629                    file_path: path.to_string(),
630                    file_size,
631                    last_modified_ms,
632                    node_id: None,
633                };
634
635                yield entry;
636            }
637        }
638    }
639}
640
641/// `OperationType` represents the origin of the `SstWriteRequest`.
642#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
643pub enum OperationType {
644    Flush,
645    Compact,
646}
647
648/// Contents to build a SST.
649pub struct SstWriteRequest {
650    pub op_type: OperationType,
651    pub metadata: RegionMetadataRef,
652    pub source: FlatSource,
653    pub cache_manager: CacheManagerRef,
654    /// Optional uniform row sequence for writes that do not preserve sequences.
655    /// Compaction and LastNonNull flushes pass `None` to retain input sequences.
656    pub max_sequence: Option<SequenceNumber>,
657    pub sst_write_format: FormatType,
658
659    pub preserve_row_sequence: bool,
660
661    /// Configs for index
662    pub index_options: IndexOptions,
663    pub index_config: IndexConfig,
664    pub inverted_index_config: InvertedIndexConfig,
665    pub fulltext_index_config: FulltextIndexConfig,
666    pub bloom_filter_index_config: BloomFilterConfig,
667}
668
669/// Index inputs for SST bytes that were encoded outside [ParquetWriter].
670pub(crate) struct EncodedSstIndexBuildParams {
671    pub(crate) metadata: RegionMetadataRef,
672    pub(crate) index_options: IndexOptions,
673    pub(crate) inverted_index_config: InvertedIndexConfig,
674    pub(crate) fulltext_index_config: FulltextIndexConfig,
675    pub(crate) bloom_filter_index_config: BloomFilterConfig,
676}
677
678impl EncodedSstIndexBuildParams {
679    pub(crate) fn indexer_builder(
680        self,
681        puffin_manager: SstPuffinManager,
682        write_cache_enabled: bool,
683        intermediate_manager: IntermediateManager,
684    ) -> IndexerBuilderImpl {
685        IndexerBuilderImpl {
686            build_type: OperationType::Flush.into(),
687            metadata: self.metadata,
688            puffin_manager,
689            write_cache_enabled,
690            intermediate_manager,
691            index_options: self.index_options,
692            inverted_index_config: self.inverted_index_config,
693            fulltext_index_config: self.fulltext_index_config,
694            bloom_filter_index_config: self.bloom_filter_index_config,
695        }
696    }
697}
698
699/// Cleaner to remove temp files on the atomic write dir.
700pub(crate) struct TempFileCleaner {
701    region_id: RegionId,
702    object_store: ObjectStore,
703}
704
705impl TempFileCleaner {
706    /// Constructs the cleaner for the region and store.
707    pub(crate) fn new(region_id: RegionId, object_store: ObjectStore) -> Self {
708        Self {
709            region_id,
710            object_store,
711        }
712    }
713
714    /// Removes the SST and index file from the local atomic dir by the file id.
715    /// This only removes the initial index, since the index version is always 0 for a new SST, this method should be safe to pass 0.
716    pub(crate) async fn clean_by_file_id(&self, file_id: FileId) {
717        let sst_key = IndexKey::new(self.region_id, file_id, FileType::Parquet).to_string();
718        let index_key = IndexKey::new(self.region_id, file_id, FileType::Puffin(0)).to_string();
719
720        Self::clean_atomic_dir_files(&self.object_store, &[&sst_key, &index_key]).await;
721    }
722
723    /// Removes the files from the local atomic dir by their names.
724    pub(crate) async fn clean_atomic_dir_files(
725        local_store: &ObjectStore,
726        names_to_remove: &[&str],
727    ) {
728        // We don't know the actual suffix of the file under atomic dir, so we have
729        // to list the dir. The cost should be acceptable as there won't be to many files.
730        let Ok(entries) = local_store.list(ATOMIC_WRITE_DIR).await.inspect_err(|e| {
731            if e.kind() != ErrorKind::NotFound {
732                common_telemetry::error!(e; "Failed to list tmp files for {:?}", names_to_remove)
733            }
734        }) else {
735            return;
736        };
737
738        // In our case, we can ensure the file id is unique so it is safe to remove all files
739        // with the same file id under the atomic write dir.
740        let actual_files: Vec<_> = entries
741            .into_iter()
742            .filter_map(|entry| {
743                if entry.metadata().is_dir() {
744                    return None;
745                }
746
747                // Remove name that matches files_to_remove.
748                let should_remove = names_to_remove
749                    .iter()
750                    .any(|file| entry.name().starts_with(file));
751                if should_remove {
752                    Some(entry.path().to_string())
753                } else {
754                    None
755                }
756            })
757            .collect();
758
759        common_telemetry::warn!(
760            "Clean files {:?} under atomic write dir for {:?}",
761            actual_files,
762            names_to_remove
763        );
764
765        if let Err(e) = local_store.delete_iter(actual_files).await {
766            common_telemetry::error!(e; "Failed to delete tmp file for {:?}", names_to_remove);
767        }
768    }
769}
770
771pub(crate) async fn new_fs_cache_store(root: &str) -> Result<ObjectStore> {
772    // Preserve native filesystem prefixes such as Windows UNC shares.
773    let atomic_write_dir = Path::new(root).join(ATOMIC_WRITE_DIR);
774    clean_dir(&atomic_write_dir.to_string_lossy()).await?;
775
776    // Compatible code. Remove this after a major release.
777    let old_atomic_temp_dir = join_dir(root, OLD_ATOMIC_WRITE_DIR);
778    clean_dir(&old_atomic_temp_dir).await?;
779
780    let builder = Fs::default()
781        .root(root)
782        .atomic_write_dir(&atomic_write_dir.to_string_lossy());
783    let store = ObjectStore::new(builder).context(OpenDalSnafu)?;
784
785    Ok(with_instrument_layers(store, false))
786}
787
788/// Clean the directory.
789async fn clean_dir(dir: &str) -> Result<()> {
790    if tokio::fs::try_exists(dir)
791        .await
792        .context(CleanDirSnafu { dir })?
793    {
794        tokio::fs::remove_dir_all(dir)
795            .await
796            .context(CleanDirSnafu { dir })?;
797    }
798
799    Ok(())
800}
801
802/// Path provider for SST file and index file.
803pub trait FilePathProvider: Send + Sync {
804    /// Creates index file path of given file id. Version default to 0, and not shown in the path.
805    fn build_index_file_path(&self, file_id: RegionFileId) -> String;
806
807    /// Creates index file path of given index id (with version support).
808    fn build_index_file_path_with_version(&self, index_id: RegionIndexId) -> String;
809
810    /// Creates SST file path of given file id.
811    fn build_sst_file_path(&self, file_id: RegionFileId) -> String;
812}
813
814/// Path provider that builds paths in local write cache.
815#[derive(Clone)]
816pub(crate) struct WriteCachePathProvider {
817    file_cache: FileCacheRef,
818}
819
820impl WriteCachePathProvider {
821    /// Creates a new `WriteCachePathProvider` instance.
822    pub fn new(file_cache: FileCacheRef) -> Self {
823        Self { file_cache }
824    }
825}
826
827impl FilePathProvider for WriteCachePathProvider {
828    fn build_index_file_path(&self, file_id: RegionFileId) -> String {
829        let puffin_key = IndexKey::new(file_id.region_id(), file_id.file_id(), FileType::Puffin(0));
830        self.file_cache.cache_file_path(puffin_key)
831    }
832
833    fn build_index_file_path_with_version(&self, index_id: RegionIndexId) -> String {
834        let puffin_key = IndexKey::new(
835            index_id.region_id(),
836            index_id.file_id(),
837            FileType::Puffin(index_id.version),
838        );
839        self.file_cache.cache_file_path(puffin_key)
840    }
841
842    fn build_sst_file_path(&self, file_id: RegionFileId) -> String {
843        let parquet_file_key =
844            IndexKey::new(file_id.region_id(), file_id.file_id(), FileType::Parquet);
845        self.file_cache.cache_file_path(parquet_file_key)
846    }
847}
848
849/// Path provider that builds paths in region storage path.
850#[derive(Clone, Debug)]
851pub(crate) struct RegionFilePathFactory {
852    pub(crate) table_dir: String,
853    pub(crate) path_type: PathType,
854}
855
856impl RegionFilePathFactory {
857    /// Creates a new `RegionFilePathFactory` instance.
858    pub fn new(table_dir: String, path_type: PathType) -> Self {
859        Self {
860            table_dir,
861            path_type,
862        }
863    }
864}
865
866impl FilePathProvider for RegionFilePathFactory {
867    fn build_index_file_path(&self, file_id: RegionFileId) -> String {
868        location::index_file_path_legacy(&self.table_dir, file_id, self.path_type)
869    }
870
871    fn build_index_file_path_with_version(&self, index_id: RegionIndexId) -> String {
872        location::index_file_path(&self.table_dir, index_id, self.path_type)
873    }
874
875    fn build_sst_file_path(&self, file_id: RegionFileId) -> String {
876        location::sst_file_path(&self.table_dir, file_id, self.path_type)
877    }
878}
879
880#[cfg(test)]
881mod tests {
882    use std::path::PathBuf;
883
884    use bytes::Bytes;
885    use common_test_util::temp_dir::create_temp_dir;
886
887    use super::*;
888    use crate::cache::CacheManager;
889    use crate::cache::test_util::new_fs_store;
890    use crate::test_util::TestEnv;
891    use crate::test_util::sst_util::WriteChunkRecorder;
892
893    #[rstest::rstest]
894    #[tokio::test]
895    async fn test_put_sst_uses_configured_buffer_size(
896        #[values(false, true)] enable_write_cache: bool,
897        #[values(1024, 4096)] chunk_size: usize,
898    ) {
899        let mut env = TestEnv::new().await;
900        let remote_chunks = WriteChunkRecorder::default();
901        let remote_store = env.init_object_store_manager().layer(remote_chunks.layer());
902        let local_dir = create_temp_dir("encoded-sst-cache");
903        let local_chunks = WriteChunkRecorder::default();
904        let local_store =
905            new_fs_store(local_dir.path().to_str().unwrap()).layer(local_chunks.layer());
906        let write_cache = if enable_write_cache {
907            Some(
908                env.create_write_cache(local_store.clone(), ReadableSize::mb(10))
909                    .await,
910            )
911        } else {
912            None
913        };
914        let cache_manager = Arc::new(
915            CacheManager::builder()
916                .write_cache(write_cache.clone())
917                .build(),
918        );
919        let access_layer = AccessLayer::new(
920            "test",
921            PathType::Bare,
922            remote_store.clone(),
923            env.get_puffin_manager(),
924            env.get_intermediate_manager(),
925        );
926        let file_id = RegionFileId::new(RegionId::new(1024, 1), FileId::random());
927        // Two full chunks and a partial tail distinguish the configured size from the default.
928        let encoded = Bytes::from(
929            (0..2 * chunk_size + 17)
930                .map(|i| (i % 251) as u8)
931                .collect::<Vec<_>>(),
932        );
933        access_layer
934            .put_sst(
935                &encoded,
936                file_id.region_id(),
937                &mut SstInfo {
938                    file_id: file_id.file_id(),
939                    ..Default::default()
940                },
941                &cache_manager,
942                ReadableSize(chunk_size as u64),
943                None,
944            )
945            .await
946            .unwrap();
947
948        let remote_path = location::sst_file_path("test", file_id, PathType::Bare);
949        remote_chunks.assert_chunks(&remote_path, chunk_size, encoded.len());
950        assert_eq!(
951            remote_store.read(&remote_path).await.unwrap().to_bytes(),
952            encoded
953        );
954        if let Some(write_cache) = write_cache {
955            let key = IndexKey::new(file_id.region_id(), file_id.file_id(), FileType::Parquet);
956            let cache_path = write_cache.file_cache().cache_file_path(key);
957            local_chunks.assert_chunks(&cache_path, chunk_size, encoded.len());
958            assert_eq!(
959                local_store.read(&cache_path).await.unwrap().to_bytes(),
960                encoded
961            );
962        }
963    }
964
965    #[rstest::rstest]
966    #[case::flush(OperationType::Flush, false)]
967    #[case::local_compaction(OperationType::Compact, false)]
968    #[case::input_only_compaction(OperationType::Compact, true)]
969    #[tokio::test]
970    async fn test_written_sst_metadata_cache_policy(
971        #[case] op_type: OperationType,
972        #[case] input_only: bool,
973    ) {
974        use crate::cache::CacheStrategy;
975        use crate::sst::file::FileMeta;
976        use crate::sst::file_purger::NoopFilePurger;
977        use crate::sst::parquet::reader::MetadataCacheMetrics;
978        use crate::test_util::sst_util::{
979            new_flat_source_from_record_batches, new_record_batch_by_range, sst_region_metadata,
980        };
981
982        let mut env = TestEnv::new().await;
983        let store = env.init_object_store_manager();
984        let access_layer = AccessLayer::new(
985            "test",
986            PathType::Bare,
987            store,
988            env.get_puffin_manager(),
989            env.get_intermediate_manager(),
990        );
991        let builder = CacheManager::builder().sst_meta_cache_size(1024 * 1024);
992        let cache = Arc::new(if input_only {
993            builder.disable_sst_meta_cache_on_write().build()
994        } else {
995            builder.build()
996        });
997        let metadata = Arc::new(sst_region_metadata());
998        let region_id = metadata.region_id;
999        let infos =
1000            access_layer
1001                .write_sst(
1002                    SstWriteRequest {
1003                        op_type,
1004                        metadata,
1005                        source: new_flat_source_from_record_batches(vec![
1006                            new_record_batch_by_range(&["a", "b"], 0, 16),
1007                        ]),
1008                        cache_manager: cache.clone(),
1009                        max_sequence: None,
1010                        sst_write_format: FormatType::Flat,
1011                        preserve_row_sequence: false,
1012                        index_options: Default::default(),
1013                        index_config: Default::default(),
1014                        inverted_index_config: Default::default(),
1015                        fulltext_index_config: Default::default(),
1016                        bloom_filter_index_config: Default::default(),
1017                    },
1018                    &WriteOptions::default(),
1019                    &mut Metrics::new(WriteType::Compaction),
1020                )
1021                .await
1022                .unwrap();
1023        assert_eq!(1, infos.len());
1024        // A prewarming task owns a cache reference until both tiers are populated.
1025        // Drain it before checking absence so the negative assertion cannot race it.
1026        tokio::time::timeout(Duration::from_secs(10), async {
1027            while Arc::strong_count(&cache) > 1 {
1028                tokio::task::yield_now().await;
1029            }
1030        })
1031        .await
1032        .unwrap();
1033        let info = &infos[0];
1034        let file_id = RegionFileId::new(region_id, info.file_id);
1035        let cached = cache
1036            .get_sst_meta_data(
1037                file_id,
1038                &mut MetadataCacheMetrics::default(),
1039                PageIndexPolicy::Skip,
1040                &common_runtime::compact_runtime(),
1041            )
1042            .await;
1043        assert_eq!(!input_only, cached.is_some());
1044
1045        let file = FileHandle::new(
1046            FileMeta {
1047                region_id,
1048                file_id: info.file_id,
1049                file_size: info.file_size,
1050                ..Default::default()
1051            },
1052            Arc::new(NoopFilePurger),
1053        );
1054        let path = file.file_path(access_layer.table_dir(), access_layer.path_type());
1055        let reader = access_layer
1056            .read_sst(file)
1057            .cache(CacheStrategy::Compaction(cache));
1058        let mut cold = MetadataCacheMetrics::default();
1059        let (first, _) = reader
1060            .read_parquet_metadata(&path, info.file_size, &mut cold, PageIndexPolicy::Skip)
1061            .await
1062            .unwrap();
1063        assert_eq!(input_only, cold.num_reads > 0);
1064        let mut warm = MetadataCacheMetrics::default();
1065        let (second, _) = reader
1066            .read_parquet_metadata(&path, info.file_size, &mut warm, PageIndexPolicy::Skip)
1067            .await
1068            .unwrap();
1069        assert_eq!(16, second.parquet_metadata().file_metadata().num_rows());
1070        assert!(Arc::ptr_eq(&first, &second));
1071        assert_eq!(0, warm.num_reads);
1072        assert_eq!(1, warm.mem_cache_hit);
1073    }
1074
1075    #[tokio::test]
1076    async fn test_new_fs_cache_store() {
1077        let root = common_test_util::temp_dir::create_temp_dir("fs-cache-store");
1078        let dirs = [
1079            root.path().join(ATOMIC_WRITE_DIR),
1080            PathBuf::from(join_dir(
1081                root.path().to_str().unwrap(),
1082                OLD_ATOMIC_WRITE_DIR,
1083            )),
1084        ];
1085        for dir in &dirs {
1086            tokio::fs::create_dir_all(dir).await.unwrap();
1087            tokio::fs::write(dir.join("stale"), b"stale").await.unwrap();
1088        }
1089        let store = new_fs_cache_store(root.path().to_str().unwrap())
1090            .await
1091            .unwrap();
1092        for dir in &dirs {
1093            assert!(!dir.join("stale").exists());
1094        }
1095        store.write("index", "contents").await.unwrap();
1096        assert_eq!(
1097            tokio::fs::read(root.path().join("index")).await.unwrap(),
1098            b"contents"
1099        );
1100    }
1101}