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