Skip to main content

mito2/cache/
write_cache.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
15//! A write-through cache for remote object stores.
16
17use std::sync::Arc;
18use std::time::{Duration, Instant};
19
20use common_base::readable_size::ReadableSize;
21use common_telemetry::{debug, info};
22use futures::AsyncWriteExt;
23use object_store::ObjectStore;
24use snafu::ResultExt;
25use store_api::storage::RegionId;
26use tokio::sync::mpsc::{UnboundedSender, unbounded_channel};
27
28use crate::access_layer::{
29    FilePathProvider, Metrics, OperationType, RegionFilePathFactory, SstInfoArray, SstWriteRequest,
30    TempFileCleaner, WriteCachePathProvider, WriteType, new_fs_cache_store,
31};
32use crate::cache::file_cache::{FileCache, FileCacheRef, FileType, IndexKey, IndexValue};
33use crate::cache::manifest_cache::ManifestCache;
34use crate::error::{self, Result};
35use crate::metrics::UPLOAD_BYTES_TOTAL;
36use crate::region::opener::RegionLoadCacheTask;
37use crate::sst::file::RegionFileId;
38use crate::sst::index::IndexerBuilderImpl;
39use crate::sst::index::intermediate::IntermediateManager;
40use crate::sst::index::puffin_manager::{PuffinManagerFactory, SstPuffinManager};
41use crate::sst::parquet::WriteOptions;
42use crate::sst::parquet::writer::ParquetWriter;
43use crate::sst::{DEFAULT_WRITE_BUFFER_SIZE, DEFAULT_WRITE_CONCURRENCY};
44
45/// Wraps the remote object store used by write cache uploads.
46pub trait WriteCacheUploadStoreWrapper: Send + Sync {
47    /// Wraps an object store before uploading a cached file.
48    ///
49    /// `op_type` identifies the origin of the upload so implementations can
50    /// apply different policies per operation, e.g. throttling compaction
51    /// uploads but not flush uploads. Index rebuild uploads are reported as
52    /// [`OperationType::Compact`].
53    fn wrap(&self, store: ObjectStore, op_type: OperationType) -> ObjectStore;
54}
55
56pub type WriteCacheUploadStoreWrapperRef = Arc<dyn WriteCacheUploadStoreWrapper>;
57
58/// A cache for uploading files to remote object stores.
59///
60/// It keeps files in local disk and then sends files to object stores.
61pub struct WriteCache {
62    /// Local file cache.
63    file_cache: FileCacheRef,
64    /// Puffin manager factory for index.
65    puffin_manager_factory: PuffinManagerFactory,
66    /// Intermediate manager for index.
67    intermediate_manager: IntermediateManager,
68    /// Sender for region load cache tasks.
69    task_sender: UnboundedSender<RegionLoadCacheTask>,
70    /// Optional cache for manifest files.
71    manifest_cache: Option<ManifestCache>,
72    /// Optional wrapper for remote stores used by uploads.
73    upload_store_wrapper: Option<WriteCacheUploadStoreWrapperRef>,
74}
75
76pub type WriteCacheRef = Arc<WriteCache>;
77
78impl WriteCache {
79    /// Create the cache with a `local_store` to cache files and a
80    /// `object_store_manager` for all object stores.
81    #[allow(clippy::too_many_arguments)]
82    pub async fn new(
83        local_store: ObjectStore,
84        cache_capacity: ReadableSize,
85        ttl: Option<Duration>,
86        index_cache_percent: Option<u8>,
87        enable_background_worker: bool,
88        puffin_manager_factory: PuffinManagerFactory,
89        intermediate_manager: IntermediateManager,
90        manifest_cache: Option<ManifestCache>,
91    ) -> Result<Self> {
92        let (task_sender, task_receiver) = unbounded_channel();
93
94        let file_cache = Arc::new(FileCache::new(
95            local_store,
96            cache_capacity,
97            ttl,
98            index_cache_percent,
99            enable_background_worker,
100        ));
101        file_cache.recover(false, Some(task_receiver)).await;
102
103        Ok(Self {
104            file_cache,
105            puffin_manager_factory,
106            intermediate_manager,
107            task_sender,
108            manifest_cache,
109            upload_store_wrapper: None,
110        })
111    }
112
113    /// Creates a write cache based on local fs.
114    #[allow(clippy::too_many_arguments)]
115    pub async fn new_fs(
116        cache_dir: &str,
117        cache_capacity: ReadableSize,
118        ttl: Option<Duration>,
119        index_cache_percent: Option<u8>,
120        enable_background_worker: bool,
121        puffin_manager_factory: PuffinManagerFactory,
122        intermediate_manager: IntermediateManager,
123        manifest_cache_capacity: ReadableSize,
124    ) -> Result<Self> {
125        info!("Init write cache on {cache_dir}, capacity: {cache_capacity}");
126
127        let local_store = new_fs_cache_store(cache_dir).await?;
128
129        // Create manifest cache if capacity is non-zero
130        let manifest_cache = if manifest_cache_capacity.as_bytes() > 0 {
131            Some(ManifestCache::new(local_store.clone(), manifest_cache_capacity, ttl, false).await)
132        } else {
133            None
134        };
135
136        Self::new(
137            local_store,
138            cache_capacity,
139            ttl,
140            index_cache_percent,
141            enable_background_worker,
142            puffin_manager_factory,
143            intermediate_manager,
144            manifest_cache,
145        )
146        .await
147    }
148
149    /// Returns the file cache of the write cache.
150    pub(crate) fn file_cache(&self) -> FileCacheRef {
151        self.file_cache.clone()
152    }
153
154    /// Returns the manifest cache if available.
155    pub(crate) fn manifest_cache(&self) -> Option<ManifestCache> {
156        self.manifest_cache.clone()
157    }
158
159    /// Sets the wrapper for remote stores used by uploads.
160    pub(crate) fn with_upload_store_wrapper(
161        mut self,
162        upload_store_wrapper: Option<WriteCacheUploadStoreWrapperRef>,
163    ) -> Self {
164        self.upload_store_wrapper = upload_store_wrapper;
165        self
166    }
167
168    /// Build the puffin manager
169    pub(crate) fn build_puffin_manager(&self) -> SstPuffinManager {
170        let store = self.file_cache.local_store();
171        let path_provider = WriteCachePathProvider::new(self.file_cache.clone());
172        self.puffin_manager_factory.build(store, path_provider)
173    }
174
175    /// Put encoded SST data to the cache and upload to the remote object store.
176    pub(crate) async fn put_and_upload_sst(
177        &self,
178        data: &bytes::Bytes,
179        region_file_id: RegionFileId,
180        upload_request: SstUploadRequest,
181        write_buffer_size: ReadableSize,
182    ) -> Result<Metrics> {
183        let region_id = region_file_id.region_id();
184        let file_id = region_file_id.file_id();
185        let mut metrics = Metrics::new(WriteType::Flush);
186
187        // Create index key for the SST file
188        let parquet_key = IndexKey::new(region_id, file_id, FileType::Parquet);
189
190        // Write to cache first
191        let cache_start = Instant::now();
192        let cache_path = self.file_cache.cache_file_path(parquet_key);
193        let store = self.file_cache.local_store();
194        let cleaner = TempFileCleaner::new(region_id, store.clone());
195        let write_res = store
196            .write_with(&cache_path, data.clone())
197            .chunk(write_buffer_size.as_bytes() as usize)
198            .await
199            .context(crate::error::OpenDalSnafu);
200        if let Err(e) = write_res {
201            cleaner.clean_by_file_id(file_id).await;
202            return Err(e);
203        }
204
205        metrics.write_batch = cache_start.elapsed();
206
207        // Upload to remote store
208        let upload_start = Instant::now();
209        let remote_path = upload_request
210            .dest_path_provider
211            .build_sst_file_path(region_file_id);
212
213        if let Err(e) = self
214            .upload(
215                parquet_key,
216                &remote_path,
217                &upload_request.remote_store,
218                UploadOptions {
219                    // `put_and_upload_sst` is only used by the flush path.
220                    op_type: OperationType::Flush,
221                    write_buffer_size,
222                },
223            )
224            .await
225        {
226            // Clean up cache on failure
227            self.remove(parquet_key).await;
228            return Err(e);
229        }
230
231        metrics.upload_parquet = upload_start.elapsed();
232        Ok(metrics)
233    }
234
235    /// Returns the intermediate manager of the write cache.
236    pub(crate) fn intermediate_manager(&self) -> &IntermediateManager {
237        &self.intermediate_manager
238    }
239
240    /// Writes SST to the cache and then uploads it to the remote object store.
241    pub(crate) async fn write_and_upload_sst(
242        &self,
243        write_request: SstWriteRequest,
244        upload_request: SstUploadRequest,
245        write_opts: &WriteOptions,
246        metrics: &mut Metrics,
247    ) -> Result<SstInfoArray> {
248        let region_id = write_request.metadata.region_id;
249        let override_sequence = if write_request.preserve_row_sequence {
250            None
251        } else {
252            write_request.max_sequence
253        };
254
255        let store = self.file_cache.local_store();
256        let path_provider = WriteCachePathProvider::new(self.file_cache.clone());
257        let indexer = IndexerBuilderImpl {
258            build_type: write_request.op_type.into(),
259            metadata: write_request.metadata.clone(),
260            puffin_manager: self
261                .puffin_manager_factory
262                .build(store.clone(), path_provider.clone()),
263            write_cache_enabled: true,
264            intermediate_manager: self.intermediate_manager.clone(),
265            index_options: write_request.index_options,
266            inverted_index_config: write_request.inverted_index_config,
267            fulltext_index_config: write_request.fulltext_index_config,
268            bloom_filter_index_config: write_request.bloom_filter_index_config,
269        };
270
271        let cleaner = TempFileCleaner::new(region_id, store.clone());
272        // Write to FileCache.
273        let mut writer = ParquetWriter::new_with_object_store(
274            store.clone(),
275            write_request.metadata,
276            write_request.index_config,
277            indexer,
278            path_provider.clone(),
279            metrics,
280        )
281        .await
282        .with_file_cleaner(cleaner);
283
284        let sst_info = match write_request.sst_write_format {
285            crate::sst::FormatType::PrimaryKey => {
286                writer
287                    .write_all_flat_as_primary_key(
288                        write_request.source,
289                        override_sequence,
290                        write_opts,
291                    )
292                    .await?
293            }
294            crate::sst::FormatType::Flat => {
295                writer
296                    .write_all_flat(write_request.source, override_sequence, write_opts)
297                    .await?
298            }
299        };
300
301        // Upload sst file to remote object store.
302        if sst_info.is_empty() {
303            return Ok(sst_info);
304        }
305
306        let mut upload_tracker = UploadTracker::new(region_id);
307        let mut err = None;
308        let op_type = write_request.op_type;
309        let remote_store = &upload_request.remote_store;
310        for sst in &sst_info {
311            let parquet_key = IndexKey::new(region_id, sst.file_id, FileType::Parquet);
312            let parquet_path = upload_request
313                .dest_path_provider
314                .build_sst_file_path(RegionFileId::new(region_id, sst.file_id));
315            let start = Instant::now();
316            if let Err(e) = self
317                .upload(
318                    parquet_key,
319                    &parquet_path,
320                    remote_store,
321                    UploadOptions {
322                        op_type,
323                        write_buffer_size: write_opts.write_buffer_size,
324                    },
325                )
326                .await
327            {
328                err = Some(e);
329                break;
330            }
331            metrics.upload_parquet += start.elapsed();
332            upload_tracker.push_uploaded_file(parquet_path);
333
334            if sst.index_metadata.file_size > 0 {
335                let puffin_key = IndexKey::new(region_id, sst.file_id, FileType::Puffin(0));
336                let puffin_path = upload_request
337                    .dest_path_provider
338                    .build_index_file_path(RegionFileId::new(region_id, sst.file_id));
339                let start = Instant::now();
340                if let Err(e) = self
341                    .upload(
342                        puffin_key,
343                        &puffin_path,
344                        remote_store,
345                        UploadOptions {
346                            op_type,
347                            write_buffer_size: DEFAULT_WRITE_BUFFER_SIZE,
348                        },
349                    )
350                    .await
351                {
352                    err = Some(e);
353                    break;
354                }
355                metrics.upload_puffin += start.elapsed();
356                upload_tracker.push_uploaded_file(puffin_path);
357            }
358        }
359
360        if let Some(err) = err {
361            // Cleans files on failure.
362            upload_tracker
363                .clean(&sst_info, &self.file_cache, remote_store)
364                .await;
365            return Err(err);
366        }
367
368        Ok(sst_info)
369    }
370
371    /// Removes a file from the cache by `index_key`.
372    pub(crate) async fn remove(&self, index_key: IndexKey) {
373        self.file_cache.remove(index_key).await
374    }
375
376    /// Downloads a file in `remote_path` from the remote object store to the local cache
377    /// (specified by `index_key`).
378    pub(crate) async fn download(
379        &self,
380        index_key: IndexKey,
381        remote_path: &str,
382        remote_store: &ObjectStore,
383        file_size: u64,
384    ) -> Result<()> {
385        self.file_cache
386            .download(index_key, remote_path, remote_store, file_size)
387            .await
388    }
389
390    /// Downloads the target file into write cache only when it is not cached.
391    ///
392    /// Returns `Ok(true)` if this call performs a download, or `Ok(false)` if the
393    /// file is already present in write cache and download is skipped.
394    pub(crate) async fn download_if_absent(
395        &self,
396        index_key: IndexKey,
397        remote_path: &str,
398        remote_store: &ObjectStore,
399        file_size: u64,
400    ) -> Result<bool> {
401        if self.file_cache.contains_key(&index_key) {
402            debug!(
403                "Skip downloading file already in write cache, region: {}, file: {}",
404                index_key.region_id, index_key.file_id
405            );
406            return Ok(false);
407        }
408
409        self.download(index_key, remote_path, remote_store, file_size)
410            .await?;
411        Ok(true)
412    }
413
414    /// Uploads a Parquet file or a Puffin file to the remote object store.
415    pub(crate) async fn upload(
416        &self,
417        index_key: IndexKey,
418        upload_path: &str,
419        remote_store: &ObjectStore,
420        options: UploadOptions,
421    ) -> Result<()> {
422        let region_id = index_key.region_id;
423        let file_id = index_key.file_id;
424        let file_type = index_key.file_type;
425        let cache_path = self.file_cache.cache_file_path(index_key);
426
427        let start = Instant::now();
428        let cached_value = self
429            .file_cache
430            .local_store()
431            .stat(&cache_path)
432            .await
433            .context(error::OpenDalSnafu)?;
434        let reader = self
435            .file_cache
436            .local_store()
437            .reader(&cache_path)
438            .await
439            .context(error::OpenDalSnafu)?
440            .into_futures_async_read(0..cached_value.content_length())
441            .await
442            .context(error::OpenDalSnafu)?;
443
444        let upload_store = self.upload_store_wrapper.as_ref().map_or_else(
445            || remote_store.clone(),
446            |wrapper| wrapper.wrap(remote_store.clone(), options.op_type),
447        );
448        let mut writer = upload_store
449            .writer_with(upload_path)
450            .chunk(options.write_buffer_size.as_bytes() as usize)
451            .concurrent(DEFAULT_WRITE_CONCURRENCY)
452            .await
453            .context(error::OpenDalSnafu)?
454            .into_futures_async_write();
455
456        let bytes_written =
457            futures::io::copy(reader, &mut writer)
458                .await
459                .context(error::UploadSnafu {
460                    region_id,
461                    file_id,
462                    file_type,
463                })?;
464
465        // Must close to upload all data.
466        writer.close().await.context(error::UploadSnafu {
467            region_id,
468            file_id,
469            file_type,
470        })?;
471
472        UPLOAD_BYTES_TOTAL.inc_by(bytes_written);
473
474        debug!(
475            "Successfully upload file to remote, region: {}, file: {}, upload_path: {}, cost: {:?}",
476            region_id,
477            file_id,
478            upload_path,
479            start.elapsed(),
480        );
481
482        let index_value = IndexValue {
483            file_size: bytes_written as _,
484        };
485        // Register to file cache
486        self.file_cache.put(index_key, index_value).await;
487
488        Ok(())
489    }
490
491    /// Sends a region load cache task to the background processing queue.
492    ///
493    /// If the receiver has been dropped, the error is ignored.
494    pub(crate) fn load_region_cache(&self, task: RegionLoadCacheTask) {
495        let _ = self.task_sender.send(task);
496    }
497}
498
499/// Request to write and upload a SST.
500pub struct SstUploadRequest {
501    /// Destination path provider of which SST files in write cache should be uploaded to.
502    pub dest_path_provider: RegionFilePathFactory,
503    /// Remote object store to upload.
504    pub remote_store: ObjectStore,
505}
506
507/// Policies for uploading a cached SST or index file.
508pub(crate) struct UploadOptions {
509    /// Origin of the upload, used by the remote store wrapper.
510    pub op_type: OperationType,
511    /// Buffer size for the remote writer.
512    pub write_buffer_size: ReadableSize,
513}
514
515/// A structs to track files to upload and clean them if upload failed.
516pub(crate) struct UploadTracker {
517    /// Id of the region to track.
518    region_id: RegionId,
519    /// Paths of files uploaded successfully.
520    files_uploaded: Vec<String>,
521}
522
523impl UploadTracker {
524    /// Creates a new instance of `UploadTracker` for a given region.
525    pub(crate) fn new(region_id: RegionId) -> Self {
526        Self {
527            region_id,
528            files_uploaded: Vec::new(),
529        }
530    }
531
532    /// Add a file path to the list of uploaded files.
533    pub(crate) fn push_uploaded_file(&mut self, path: String) {
534        self.files_uploaded.push(path);
535    }
536
537    /// Cleans uploaded files and files in the file cache at best effort.
538    pub(crate) async fn clean(
539        &self,
540        sst_info: &SstInfoArray,
541        file_cache: &FileCacheRef,
542        remote_store: &ObjectStore,
543    ) {
544        common_telemetry::info!(
545            "Start cleaning files on upload failure, region: {}, num_ssts: {}",
546            self.region_id,
547            sst_info.len()
548        );
549
550        // Cleans files in the file cache first.
551        for sst in sst_info {
552            let parquet_key = IndexKey::new(self.region_id, sst.file_id, FileType::Parquet);
553            file_cache.remove(parquet_key).await;
554
555            if sst.index_metadata.file_size > 0 {
556                let puffin_key = IndexKey::new(
557                    self.region_id,
558                    sst.file_id,
559                    FileType::Puffin(sst.index_metadata.version),
560                );
561                file_cache.remove(puffin_key).await;
562            }
563        }
564
565        // Cleans uploaded files.
566        for file_path in &self.files_uploaded {
567            if let Err(e) = remote_store.delete(file_path).await {
568                common_telemetry::error!(e; "Failed to delete file {}", file_path);
569            }
570        }
571    }
572}
573
574#[cfg(test)]
575mod tests {
576    use std::sync::Mutex;
577
578    use bytes::Bytes;
579    use common_test_util::temp_dir::create_temp_dir;
580    use object_store::services::Memory;
581    use object_store::{ATOMIC_WRITE_DIR, ObjectStore};
582    use parquet::file::metadata::PageIndexPolicy;
583    use store_api::region_request::PathType;
584    use store_api::storage::FileId;
585
586    use super::*;
587    use crate::access_layer::OperationType;
588    use crate::cache::file_cache::IndexValue;
589    use crate::cache::test_util::{assert_parquet_metadata_equal, new_fs_store};
590    use crate::cache::{CacheManager, CacheStrategy};
591    use crate::error::InvalidBatchSnafu;
592    use crate::read::FlatSource;
593    use crate::region::options::IndexOptions;
594    use crate::sst::parquet::reader::ParquetReaderBuilder;
595    use crate::test_util::TestEnv;
596    use crate::test_util::sst_util::{
597        WriteChunkRecorder, new_flat_source_from_record_batches, new_record_batch_by_range,
598        sst_file_handle_with_file_id, sst_region_metadata,
599    };
600
601    struct RedirectUploadStoreWrapper {
602        target_store: ObjectStore,
603        last_op_type: Mutex<Option<OperationType>>,
604    }
605
606    impl WriteCacheUploadStoreWrapper for RedirectUploadStoreWrapper {
607        fn wrap(&self, _store: ObjectStore, op_type: OperationType) -> ObjectStore {
608            *self.last_op_type.lock().unwrap() = Some(op_type);
609            self.target_store.clone()
610        }
611    }
612
613    #[tokio::test]
614    async fn test_upload_uses_wrapped_remote_store() {
615        let env = TestEnv::new().await;
616        let local_store = ObjectStore::new(Memory::default()).unwrap();
617        let original_store = ObjectStore::new(Memory::default()).unwrap();
618        let target_store = ObjectStore::new(Memory::default()).unwrap();
619        let wrapper = Arc::new(RedirectUploadStoreWrapper {
620            target_store: target_store.clone(),
621            last_op_type: Mutex::new(None),
622        });
623        let write_cache = WriteCache::new(
624            local_store.clone(),
625            ReadableSize::mb(10),
626            None,
627            None,
628            false,
629            env.get_puffin_manager(),
630            env.get_intermediate_manager(),
631            None,
632        )
633        .await
634        .unwrap()
635        .with_upload_store_wrapper(Some(wrapper.clone()));
636
637        let region_id = RegionId::new(1024, 1);
638        let file_id = FileId::random();
639        let key = IndexKey::new(region_id, file_id, FileType::Parquet);
640        let cache_path = write_cache.file_cache.cache_file_path(key);
641        let upload_path = "wrapped-upload.parquet";
642        let data = Bytes::from_static(b"wrapped upload data");
643        local_store.write(&cache_path, data.clone()).await.unwrap();
644
645        write_cache
646            .upload(
647                key,
648                upload_path,
649                &original_store,
650                UploadOptions {
651                    op_type: OperationType::Compact,
652                    write_buffer_size: DEFAULT_WRITE_BUFFER_SIZE,
653                },
654            )
655            .await
656            .unwrap();
657
658        assert_eq!(
659            *wrapper.last_op_type.lock().unwrap(),
660            Some(OperationType::Compact)
661        );
662
663        assert!(original_store.stat(upload_path).await.is_err());
664        assert_eq!(
665            target_store.read(upload_path).await.unwrap().to_vec(),
666            data.to_vec()
667        );
668    }
669
670    #[rstest::rstest]
671    #[tokio::test]
672    async fn test_write_and_upload_sst(
673        #[values(OperationType::Flush, OperationType::Compact)] op_type: OperationType,
674    ) {
675        // TODO(QuenKar): maybe find a way to create some object server for testing,
676        // and now just use local file system to mock.
677        let mut env = TestEnv::new().await;
678        let remote_chunks = WriteChunkRecorder::default();
679        let mock_store = env.init_object_store_manager().layer(remote_chunks.layer());
680        let path_provider = RegionFilePathFactory::new("test".to_string(), PathType::Bare);
681
682        let local_dir = create_temp_dir("");
683        let local_chunks = WriteChunkRecorder::default();
684        let local_store =
685            new_fs_store(local_dir.path().to_str().unwrap()).layer(local_chunks.layer());
686
687        let write_cache = env
688            .create_write_cache(local_store.clone(), ReadableSize::mb(10))
689            .await;
690
691        // Create source.
692        let metadata = Arc::new(sst_region_metadata());
693        let region_id = metadata.region_id;
694        let source = new_flat_source_from_record_batches(vec![
695            new_record_batch_by_range(&["a", "d"], 0, 60),
696            new_record_batch_by_range(&["b", "f"], 0, 40),
697            new_record_batch_by_range(&["b", "h"], 100, 200),
698        ]);
699
700        let write_request = SstWriteRequest {
701            op_type,
702            metadata,
703            source,
704            max_sequence: None,
705            sst_write_format: Default::default(),
706            cache_manager: Default::default(),
707            preserve_row_sequence: false,
708            index_options: IndexOptions::default(),
709            index_config: Default::default(),
710            inverted_index_config: Default::default(),
711            fulltext_index_config: Default::default(),
712            bloom_filter_index_config: Default::default(),
713        };
714
715        let upload_request = SstUploadRequest {
716            dest_path_provider: path_provider.clone(),
717            remote_store: mock_store.clone(),
718        };
719
720        let write_opts = WriteOptions {
721            write_buffer_size: ReadableSize::kb(1),
722            row_group_size: 512,
723            ..Default::default()
724        };
725
726        // Write to cache and upload sst to mock remote store
727        let mut metrics = Metrics::new(WriteType::Flush);
728        let mut sst_infos = write_cache
729            .write_and_upload_sst(write_request, upload_request, &write_opts, &mut metrics)
730            .await
731            .unwrap();
732        let sst_info = sst_infos.remove(0);
733
734        let file_id = sst_info.file_id;
735        let sst_upload_path =
736            path_provider.build_sst_file_path(RegionFileId::new(region_id, file_id));
737        let index_upload_path =
738            path_provider.build_index_file_path(RegionFileId::new(region_id, file_id));
739
740        // Check write cache contains the key
741        let key = IndexKey::new(region_id, file_id, FileType::Parquet);
742        assert!(write_cache.file_cache.contains_key(&key));
743
744        // Check file data
745        let remote_data = mock_store.read(&sst_upload_path).await.unwrap();
746        let cache_data = local_store
747            .read(&write_cache.file_cache.cache_file_path(key))
748            .await
749            .unwrap();
750        assert_eq!(remote_data.to_vec(), cache_data.to_vec());
751        let chunk_size = write_opts.write_buffer_size.as_bytes() as usize;
752        assert!(remote_data.len() > chunk_size);
753        remote_chunks.assert_chunks(&sst_upload_path, chunk_size, remote_data.len());
754        local_chunks.assert_chunks(
755            &write_cache.file_cache.cache_file_path(key),
756            chunk_size,
757            cache_data.len(),
758        );
759
760        // Check write cache contains the index key
761        let index_key = IndexKey::new(region_id, file_id, FileType::Puffin(0));
762        assert!(write_cache.file_cache.contains_key(&index_key));
763
764        let remote_index_data = mock_store.read(&index_upload_path).await.unwrap();
765        let cache_index_data = local_store
766            .read(&write_cache.file_cache.cache_file_path(index_key))
767            .await
768            .unwrap();
769        assert_eq!(remote_index_data.to_vec(), cache_index_data.to_vec());
770        remote_chunks.assert_chunks(
771            &index_upload_path,
772            DEFAULT_WRITE_BUFFER_SIZE.as_bytes() as usize,
773            remote_index_data.len(),
774        );
775
776        // Removes the file from the cache.
777        let sst_index_key = IndexKey::new(region_id, file_id, FileType::Parquet);
778        write_cache.remove(sst_index_key).await;
779        assert!(!write_cache.file_cache.contains_key(&sst_index_key));
780        write_cache.remove(index_key).await;
781        assert!(!write_cache.file_cache.contains_key(&index_key));
782    }
783
784    #[tokio::test]
785    async fn test_read_metadata_from_write_cache() {
786        common_telemetry::init_default_ut_logging();
787        let mut env = TestEnv::new().await;
788        // Object keys are relative to the mock store root, not native paths.
789        let data_home = "write-cache-test".to_string();
790        let mock_store = env.init_object_store_manager();
791
792        let local_dir = create_temp_dir("");
793        let local_path = local_dir.path().to_str().unwrap();
794        let local_store = new_fs_store(local_path);
795
796        // Create a cache manager using only write cache
797        let write_cache = env
798            .create_write_cache(local_store.clone(), ReadableSize::mb(10))
799            .await;
800        let cache_manager = Arc::new(
801            CacheManager::builder()
802                .write_cache(Some(write_cache.clone()))
803                .build(),
804        );
805        assert!(!cache_manager.sst_meta_cache_enabled());
806
807        // Create source
808        let metadata = Arc::new(sst_region_metadata());
809
810        let source = new_flat_source_from_record_batches(vec![
811            new_record_batch_by_range(&["a", "d"], 0, 60),
812            new_record_batch_by_range(&["b", "f"], 0, 40),
813            new_record_batch_by_range(&["b", "h"], 100, 200),
814        ]);
815
816        // Write to local cache and upload sst to mock remote store
817        let write_request = SstWriteRequest {
818            op_type: OperationType::Flush,
819            metadata,
820            source,
821            max_sequence: None,
822            sst_write_format: Default::default(),
823            cache_manager: cache_manager.clone(),
824            preserve_row_sequence: false,
825            index_options: IndexOptions::default(),
826            index_config: Default::default(),
827            inverted_index_config: Default::default(),
828            fulltext_index_config: Default::default(),
829            bloom_filter_index_config: Default::default(),
830        };
831        let write_opts = WriteOptions {
832            row_group_size: 512,
833            ..Default::default()
834        };
835        let upload_request = SstUploadRequest {
836            dest_path_provider: RegionFilePathFactory::new(data_home.clone(), PathType::Bare),
837            remote_store: mock_store.clone(),
838        };
839
840        let mut metrics = Metrics::new(WriteType::Flush);
841        let mut sst_infos = write_cache
842            .write_and_upload_sst(write_request, upload_request, &write_opts, &mut metrics)
843            .await
844            .unwrap();
845        let sst_info = sst_infos.remove(0);
846        let write_parquet_metadata = sst_info.file_metadata.unwrap();
847
848        // Read metadata from write cache without preparing an in-memory metadata cache entry.
849        let handle = sst_file_handle_with_file_id(sst_info.file_id, 0, 1000);
850        let builder = ParquetReaderBuilder::new(
851            data_home,
852            PathType::Bare,
853            handle.clone(),
854            mock_store.clone(),
855        )
856        .cache(CacheStrategy::EnableAll(cache_manager.clone()))
857        .page_index_policy(PageIndexPolicy::Optional);
858        let reader = builder.build().await.unwrap().unwrap();
859        let cached_write_parquet_metadata = crate::cache::CachedSstMeta::try_new(
860            "test.sst",
861            Arc::unwrap_or_clone(write_parquet_metadata),
862        )
863        .unwrap()
864        .parquet_metadata();
865
866        // Check parquet metadata
867        assert_parquet_metadata_equal(cached_write_parquet_metadata, reader.parquet_metadata());
868    }
869
870    #[tokio::test]
871    async fn test_write_cache_clean_tmp_files() {
872        common_telemetry::init_default_ut_logging();
873        let mut env = TestEnv::new().await;
874        let data_home = env.data_home().display().to_string();
875        let mock_store = env.init_object_store_manager();
876
877        let write_cache_dir = create_temp_dir("");
878        let write_cache_path = write_cache_dir.path().to_str().unwrap();
879        let write_cache = env
880            .create_write_cache_from_path(write_cache_path, ReadableSize::mb(10))
881            .await;
882
883        // Create a cache manager using only write cache
884        let cache_manager = Arc::new(
885            CacheManager::builder()
886                .write_cache(Some(write_cache.clone()))
887                .build(),
888        );
889
890        // Create source
891        let metadata = Arc::new(sst_region_metadata());
892
893        // Creates a source that can return an error to abort the writer.
894        let record_batch = new_record_batch_by_range(&["a", "d"], 0, 60);
895        let schema = record_batch.schema();
896        let iter = Box::new(
897            [
898                Ok(record_batch),
899                InvalidBatchSnafu {
900                    reason: "Abort the writer",
901                }
902                .fail(),
903            ]
904            .into_iter(),
905        );
906        let source = FlatSource::new_iter(schema, iter);
907
908        // Write to local cache and upload sst to mock remote store
909        let write_request = SstWriteRequest {
910            op_type: OperationType::Flush,
911            metadata,
912            source,
913            max_sequence: None,
914            sst_write_format: Default::default(),
915            cache_manager: cache_manager.clone(),
916            preserve_row_sequence: false,
917            index_options: IndexOptions::default(),
918            index_config: Default::default(),
919            inverted_index_config: Default::default(),
920            fulltext_index_config: Default::default(),
921            bloom_filter_index_config: Default::default(),
922        };
923        let write_opts = WriteOptions {
924            row_group_size: 512,
925            ..Default::default()
926        };
927        let upload_request = SstUploadRequest {
928            dest_path_provider: RegionFilePathFactory::new(data_home.clone(), PathType::Bare),
929            remote_store: mock_store.clone(),
930        };
931
932        let mut metrics = Metrics::new(WriteType::Flush);
933        write_cache
934            .write_and_upload_sst(write_request, upload_request, &write_opts, &mut metrics)
935            .await
936            .unwrap_err();
937        let atomic_write_dir = write_cache_dir.path().join(ATOMIC_WRITE_DIR);
938        let mut entries = tokio::fs::read_dir(&atomic_write_dir).await.unwrap();
939        let mut has_files = false;
940        while let Some(entry) = entries.next_entry().await.unwrap() {
941            if entry.file_type().await.unwrap().is_dir() {
942                continue;
943            }
944            has_files = true;
945            common_telemetry::warn!(
946                "Found remaining temporary file in atomic dir: {}",
947                entry.path().display()
948            );
949        }
950
951        assert!(!has_files);
952    }
953
954    #[tokio::test]
955    async fn test_download_if_absent_skips_when_cached() {
956        let mut env = TestEnv::new().await;
957        let remote_store = env.init_object_store_manager();
958
959        let local_dir = create_temp_dir("");
960        let local_store = new_fs_store(local_dir.path().to_str().unwrap());
961        let write_cache = env
962            .create_write_cache(local_store.clone(), ReadableSize::mb(10))
963            .await;
964
965        let region_id = RegionId::new(1024, 1);
966        let file_id = FileId::random();
967        let key = IndexKey::new(region_id, file_id, FileType::Parquet);
968        write_cache
969            .file_cache()
970            .put(key, IndexValue { file_size: 1 })
971            .await;
972
973        let downloaded = write_cache
974            .download_if_absent(key, "missing/path.parquet", &remote_store, 1)
975            .await
976            .unwrap();
977
978        assert!(!downloaded);
979    }
980
981    #[tokio::test]
982    async fn test_download_if_absent_downloads_when_missing() {
983        let mut env = TestEnv::new().await;
984        let remote_store = env.init_object_store_manager();
985
986        let local_dir = create_temp_dir("");
987        let local_store = new_fs_store(local_dir.path().to_str().unwrap());
988        let write_cache = env
989            .create_write_cache(local_store.clone(), ReadableSize::mb(10))
990            .await;
991
992        let region_id = RegionId::new(1024, 2);
993        let file_id = FileId::random();
994        let key = IndexKey::new(region_id, file_id, FileType::Parquet);
995        let remote_path = format!("download-if-absent/{file_id}.parquet");
996        let remote_data = Bytes::from_static(b"download-if-absent-test");
997        remote_store
998            .write(&remote_path, remote_data.clone())
999            .await
1000            .unwrap();
1001
1002        let downloaded = write_cache
1003            .download_if_absent(key, &remote_path, &remote_store, remote_data.len() as u64)
1004            .await
1005            .unwrap();
1006
1007        assert!(downloaded);
1008        assert!(write_cache.file_cache().contains_key(&key));
1009
1010        let cached_data = local_store
1011            .read(&write_cache.file_cache().cache_file_path(key))
1012            .await
1013            .unwrap();
1014        assert_eq!(cached_data.to_vec(), remote_data.to_vec());
1015    }
1016}