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            storage: None,
705            max_sequence: None,
706            sst_write_format: Default::default(),
707            cache_manager: Default::default(),
708            preserve_row_sequence: false,
709            index_options: IndexOptions::default(),
710            index_config: Default::default(),
711            inverted_index_config: Default::default(),
712            fulltext_index_config: Default::default(),
713            bloom_filter_index_config: Default::default(),
714        };
715
716        let upload_request = SstUploadRequest {
717            dest_path_provider: path_provider.clone(),
718            remote_store: mock_store.clone(),
719        };
720
721        let write_opts = WriteOptions {
722            write_buffer_size: ReadableSize::kb(1),
723            row_group_size: 512,
724            ..Default::default()
725        };
726
727        // Write to cache and upload sst to mock remote store
728        let mut metrics = Metrics::new(WriteType::Flush);
729        let mut sst_infos = write_cache
730            .write_and_upload_sst(write_request, upload_request, &write_opts, &mut metrics)
731            .await
732            .unwrap();
733        let sst_info = sst_infos.remove(0);
734
735        let file_id = sst_info.file_id;
736        let sst_upload_path =
737            path_provider.build_sst_file_path(RegionFileId::new(region_id, file_id));
738        let index_upload_path =
739            path_provider.build_index_file_path(RegionFileId::new(region_id, file_id));
740
741        // Check write cache contains the key
742        let key = IndexKey::new(region_id, file_id, FileType::Parquet);
743        assert!(write_cache.file_cache.contains_key(&key));
744
745        // Check file data
746        let remote_data = mock_store.read(&sst_upload_path).await.unwrap();
747        let cache_data = local_store
748            .read(&write_cache.file_cache.cache_file_path(key))
749            .await
750            .unwrap();
751        assert_eq!(remote_data.to_vec(), cache_data.to_vec());
752        let chunk_size = write_opts.write_buffer_size.as_bytes() as usize;
753        assert!(remote_data.len() > chunk_size);
754        remote_chunks.assert_chunks(&sst_upload_path, chunk_size, remote_data.len());
755        local_chunks.assert_chunks(
756            &write_cache.file_cache.cache_file_path(key),
757            chunk_size,
758            cache_data.len(),
759        );
760
761        // Check write cache contains the index key
762        let index_key = IndexKey::new(region_id, file_id, FileType::Puffin(0));
763        assert!(write_cache.file_cache.contains_key(&index_key));
764
765        let remote_index_data = mock_store.read(&index_upload_path).await.unwrap();
766        let cache_index_data = local_store
767            .read(&write_cache.file_cache.cache_file_path(index_key))
768            .await
769            .unwrap();
770        assert_eq!(remote_index_data.to_vec(), cache_index_data.to_vec());
771        remote_chunks.assert_chunks(
772            &index_upload_path,
773            DEFAULT_WRITE_BUFFER_SIZE.as_bytes() as usize,
774            remote_index_data.len(),
775        );
776
777        // Removes the file from the cache.
778        let sst_index_key = IndexKey::new(region_id, file_id, FileType::Parquet);
779        write_cache.remove(sst_index_key).await;
780        assert!(!write_cache.file_cache.contains_key(&sst_index_key));
781        write_cache.remove(index_key).await;
782        assert!(!write_cache.file_cache.contains_key(&index_key));
783    }
784
785    #[tokio::test]
786    async fn test_read_metadata_from_write_cache() {
787        common_telemetry::init_default_ut_logging();
788        let mut env = TestEnv::new().await;
789        // Object keys are relative to the mock store root, not native paths.
790        let data_home = "write-cache-test".to_string();
791        let mock_store = env.init_object_store_manager();
792
793        let local_dir = create_temp_dir("");
794        let local_path = local_dir.path().to_str().unwrap();
795        let local_store = new_fs_store(local_path);
796
797        // Create a cache manager using only write cache
798        let write_cache = env
799            .create_write_cache(local_store.clone(), ReadableSize::mb(10))
800            .await;
801        let cache_manager = Arc::new(
802            CacheManager::builder()
803                .write_cache(Some(write_cache.clone()))
804                .build(),
805        );
806        assert!(!cache_manager.sst_meta_cache_enabled());
807
808        // Create source
809        let metadata = Arc::new(sst_region_metadata());
810
811        let source = new_flat_source_from_record_batches(vec![
812            new_record_batch_by_range(&["a", "d"], 0, 60),
813            new_record_batch_by_range(&["b", "f"], 0, 40),
814            new_record_batch_by_range(&["b", "h"], 100, 200),
815        ]);
816
817        // Write to local cache and upload sst to mock remote store
818        let write_request = SstWriteRequest {
819            op_type: OperationType::Flush,
820            metadata,
821            source,
822            storage: None,
823            max_sequence: None,
824            sst_write_format: Default::default(),
825            cache_manager: cache_manager.clone(),
826            preserve_row_sequence: false,
827            index_options: IndexOptions::default(),
828            index_config: Default::default(),
829            inverted_index_config: Default::default(),
830            fulltext_index_config: Default::default(),
831            bloom_filter_index_config: Default::default(),
832        };
833        let write_opts = WriteOptions {
834            row_group_size: 512,
835            ..Default::default()
836        };
837        let upload_request = SstUploadRequest {
838            dest_path_provider: RegionFilePathFactory::new(data_home.clone(), PathType::Bare),
839            remote_store: mock_store.clone(),
840        };
841
842        let mut metrics = Metrics::new(WriteType::Flush);
843        let mut sst_infos = write_cache
844            .write_and_upload_sst(write_request, upload_request, &write_opts, &mut metrics)
845            .await
846            .unwrap();
847        let sst_info = sst_infos.remove(0);
848        let write_parquet_metadata = sst_info.file_metadata.unwrap();
849
850        // Read metadata from write cache without preparing an in-memory metadata cache entry.
851        let handle = sst_file_handle_with_file_id(sst_info.file_id, 0, 1000);
852        let builder = ParquetReaderBuilder::new(
853            data_home,
854            PathType::Bare,
855            handle.clone(),
856            mock_store.clone(),
857        )
858        .cache(CacheStrategy::EnableAll(cache_manager.clone()))
859        .page_index_policy(PageIndexPolicy::Optional);
860        let reader = builder.build().await.unwrap().unwrap();
861        let cached_write_parquet_metadata = crate::cache::CachedSstMeta::try_new(
862            "test.sst",
863            Arc::unwrap_or_clone(write_parquet_metadata),
864        )
865        .unwrap()
866        .parquet_metadata();
867
868        // Check parquet metadata
869        assert_parquet_metadata_equal(cached_write_parquet_metadata, reader.parquet_metadata());
870    }
871
872    #[tokio::test]
873    async fn test_write_cache_clean_tmp_files() {
874        common_telemetry::init_default_ut_logging();
875        let mut env = TestEnv::new().await;
876        let data_home = env.data_home().display().to_string();
877        let mock_store = env.init_object_store_manager();
878
879        let write_cache_dir = create_temp_dir("");
880        let write_cache_path = write_cache_dir.path().to_str().unwrap();
881        let write_cache = env
882            .create_write_cache_from_path(write_cache_path, ReadableSize::mb(10))
883            .await;
884
885        // Create a cache manager using only write cache
886        let cache_manager = Arc::new(
887            CacheManager::builder()
888                .write_cache(Some(write_cache.clone()))
889                .build(),
890        );
891
892        // Create source
893        let metadata = Arc::new(sst_region_metadata());
894
895        // Creates a source that can return an error to abort the writer.
896        let record_batch = new_record_batch_by_range(&["a", "d"], 0, 60);
897        let schema = record_batch.schema();
898        let iter = Box::new(
899            [
900                Ok(record_batch),
901                InvalidBatchSnafu {
902                    reason: "Abort the writer",
903                }
904                .fail(),
905            ]
906            .into_iter(),
907        );
908        let source = FlatSource::new_iter(schema, iter);
909
910        // Write to local cache and upload sst to mock remote store
911        let write_request = SstWriteRequest {
912            op_type: OperationType::Flush,
913            metadata,
914            source,
915            storage: None,
916            max_sequence: None,
917            sst_write_format: Default::default(),
918            cache_manager: cache_manager.clone(),
919            preserve_row_sequence: false,
920            index_options: IndexOptions::default(),
921            index_config: Default::default(),
922            inverted_index_config: Default::default(),
923            fulltext_index_config: Default::default(),
924            bloom_filter_index_config: Default::default(),
925        };
926        let write_opts = WriteOptions {
927            row_group_size: 512,
928            ..Default::default()
929        };
930        let upload_request = SstUploadRequest {
931            dest_path_provider: RegionFilePathFactory::new(data_home.clone(), PathType::Bare),
932            remote_store: mock_store.clone(),
933        };
934
935        let mut metrics = Metrics::new(WriteType::Flush);
936        write_cache
937            .write_and_upload_sst(write_request, upload_request, &write_opts, &mut metrics)
938            .await
939            .unwrap_err();
940        let atomic_write_dir = write_cache_dir.path().join(ATOMIC_WRITE_DIR);
941        let mut entries = tokio::fs::read_dir(&atomic_write_dir).await.unwrap();
942        let mut has_files = false;
943        while let Some(entry) = entries.next_entry().await.unwrap() {
944            if entry.file_type().await.unwrap().is_dir() {
945                continue;
946            }
947            has_files = true;
948            common_telemetry::warn!(
949                "Found remaining temporary file in atomic dir: {}",
950                entry.path().display()
951            );
952        }
953
954        assert!(!has_files);
955    }
956
957    #[tokio::test]
958    async fn test_download_if_absent_skips_when_cached() {
959        let mut env = TestEnv::new().await;
960        let remote_store = env.init_object_store_manager();
961
962        let local_dir = create_temp_dir("");
963        let local_store = new_fs_store(local_dir.path().to_str().unwrap());
964        let write_cache = env
965            .create_write_cache(local_store.clone(), ReadableSize::mb(10))
966            .await;
967
968        let region_id = RegionId::new(1024, 1);
969        let file_id = FileId::random();
970        let key = IndexKey::new(region_id, file_id, FileType::Parquet);
971        write_cache
972            .file_cache()
973            .put(key, IndexValue { file_size: 1 })
974            .await;
975
976        let downloaded = write_cache
977            .download_if_absent(key, "missing/path.parquet", &remote_store, 1)
978            .await
979            .unwrap();
980
981        assert!(!downloaded);
982    }
983
984    #[tokio::test]
985    async fn test_download_if_absent_downloads_when_missing() {
986        let mut env = TestEnv::new().await;
987        let remote_store = env.init_object_store_manager();
988
989        let local_dir = create_temp_dir("");
990        let local_store = new_fs_store(local_dir.path().to_str().unwrap());
991        let write_cache = env
992            .create_write_cache(local_store.clone(), ReadableSize::mb(10))
993            .await;
994
995        let region_id = RegionId::new(1024, 2);
996        let file_id = FileId::random();
997        let key = IndexKey::new(region_id, file_id, FileType::Parquet);
998        let remote_path = format!("download-if-absent/{file_id}.parquet");
999        let remote_data = Bytes::from_static(b"download-if-absent-test");
1000        remote_store
1001            .write(&remote_path, remote_data.clone())
1002            .await
1003            .unwrap();
1004
1005        let downloaded = write_cache
1006            .download_if_absent(key, &remote_path, &remote_store, remote_data.len() as u64)
1007            .await
1008            .unwrap();
1009
1010        assert!(downloaded);
1011        assert!(write_cache.file_cache().contains_key(&key));
1012
1013        let cached_data = local_store
1014            .read(&write_cache.file_cache().cache_file_path(key))
1015            .await
1016            .unwrap();
1017        assert_eq!(cached_data.to_vec(), remote_data.to_vec());
1018    }
1019}