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