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