1use std::path::Path;
16use std::sync::Arc;
17use std::time::{Duration, Instant};
18
19use async_stream::try_stream;
20use common_base::readable_size::ReadableSize;
21use common_runtime::runtime::RuntimeTrait;
22use common_telemetry::warn;
23use common_time::Timestamp;
24use futures::{Stream, TryStreamExt};
25use object_store::services::Fs;
26use object_store::util::{join_dir, with_instrument_layers};
27use object_store::{ATOMIC_WRITE_DIR, ErrorKind, OLD_ATOMIC_WRITE_DIR, ObjectStore};
28use parquet::file::metadata::PageIndexPolicy;
29use smallvec::SmallVec;
30use snafu::ResultExt;
31use store_api::metadata::RegionMetadataRef;
32use store_api::region_request::PathType;
33use store_api::sst_entry::StorageSstEntry;
34use store_api::storage::{FileId, RegionId, SequenceNumber};
35
36use crate::cache::file_cache::{FileCacheRef, FileType, IndexKey};
37use crate::cache::write_cache::SstUploadRequest;
38use crate::cache::{CacheManagerRef, SstMetaPreparation, prepare_sst_meta_sync};
39use crate::config::{BloomFilterConfig, FulltextIndexConfig, IndexConfig, InvertedIndexConfig};
40use crate::error::{
41 CleanDirSnafu, DeleteIndexSnafu, DeleteIndexesSnafu, DeleteSstsSnafu, OpenDalSnafu, Result,
42};
43use crate::metrics::{COMPACTION_STAGE_ELAPSED, FLUSH_ELAPSED};
44use crate::read::FlatSource;
45use crate::region::options::IndexOptions;
46use crate::sst::file::{FileHandle, RegionFileId, RegionIndexId};
47use crate::sst::index::intermediate::IntermediateManager;
48use crate::sst::index::puffin_manager::{PuffinManagerFactory, SstPuffinManager};
49use crate::sst::index::{IndexerBuilderImpl, build_index_from_parquet_bytes};
50use crate::sst::location::{self, region_dir_from_table_dir};
51use crate::sst::parquet::reader::ParquetReaderBuilder;
52use crate::sst::parquet::writer::ParquetWriter;
53use crate::sst::parquet::{SstInfo, WriteOptions};
54use crate::sst::{DEFAULT_WRITE_CONCURRENCY, FormatType};
55
56pub type AccessLayerRef = Arc<AccessLayer>;
57pub type SstInfoArray = SmallVec<[SstInfo; 2]>;
59
60#[derive(Eq, PartialEq, Debug)]
62pub enum WriteType {
63 Flush,
65 Compaction,
67}
68
69#[derive(Debug)]
70pub struct Metrics {
71 pub(crate) write_type: WriteType,
72 pub(crate) iter_source: Duration,
73 pub(crate) write_batch: Duration,
74 pub(crate) update_index: Duration,
75 pub(crate) upload_parquet: Duration,
76 pub(crate) upload_puffin: Duration,
77 pub(crate) compact_memtable: Duration,
78}
79
80impl Metrics {
81 pub fn new(write_type: WriteType) -> Self {
82 Self {
83 write_type,
84 iter_source: Default::default(),
85 write_batch: Default::default(),
86 update_index: Default::default(),
87 upload_parquet: Default::default(),
88 upload_puffin: Default::default(),
89 compact_memtable: Default::default(),
90 }
91 }
92
93 pub(crate) fn merge(mut self, other: Self) -> Self {
94 assert_eq!(self.write_type, other.write_type);
95 self.iter_source += other.iter_source;
96 self.write_batch += other.write_batch;
97 self.update_index += other.update_index;
98 self.upload_parquet += other.upload_parquet;
99 self.upload_puffin += other.upload_puffin;
100 self.compact_memtable += other.compact_memtable;
101 self
102 }
103
104 pub(crate) fn observe(self) {
105 match self.write_type {
106 WriteType::Flush => {
107 FLUSH_ELAPSED
108 .with_label_values(&["iter_source"])
109 .observe(self.iter_source.as_secs_f64());
110 FLUSH_ELAPSED
111 .with_label_values(&["write_batch"])
112 .observe(self.write_batch.as_secs_f64());
113 FLUSH_ELAPSED
114 .with_label_values(&["update_index"])
115 .observe(self.update_index.as_secs_f64());
116 FLUSH_ELAPSED
117 .with_label_values(&["upload_parquet"])
118 .observe(self.upload_parquet.as_secs_f64());
119 FLUSH_ELAPSED
120 .with_label_values(&["upload_puffin"])
121 .observe(self.upload_puffin.as_secs_f64());
122 if !self.compact_memtable.is_zero() {
123 FLUSH_ELAPSED
124 .with_label_values(&["compact_memtable"])
125 .observe(self.upload_puffin.as_secs_f64());
126 }
127 }
128 WriteType::Compaction => {
129 COMPACTION_STAGE_ELAPSED
130 .with_label_values(&["iter_source"])
131 .observe(self.iter_source.as_secs_f64());
132 COMPACTION_STAGE_ELAPSED
133 .with_label_values(&["write_batch"])
134 .observe(self.write_batch.as_secs_f64());
135 COMPACTION_STAGE_ELAPSED
136 .with_label_values(&["update_index"])
137 .observe(self.update_index.as_secs_f64());
138 COMPACTION_STAGE_ELAPSED
139 .with_label_values(&["upload_parquet"])
140 .observe(self.upload_parquet.as_secs_f64());
141 COMPACTION_STAGE_ELAPSED
142 .with_label_values(&["upload_puffin"])
143 .observe(self.upload_puffin.as_secs_f64());
144 }
145 };
146 }
147}
148
149pub struct AccessLayer {
151 table_dir: String,
152 path_type: PathType,
154 object_store: ObjectStore,
156 puffin_manager_factory: PuffinManagerFactory,
158 intermediate_manager: IntermediateManager,
160}
161
162impl std::fmt::Debug for AccessLayer {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 f.debug_struct("AccessLayer")
165 .field("table_dir", &self.table_dir)
166 .finish()
167 }
168}
169
170impl AccessLayer {
171 pub fn new(
173 table_dir: impl Into<String>,
174 path_type: PathType,
175 object_store: ObjectStore,
176 puffin_manager_factory: PuffinManagerFactory,
177 intermediate_manager: IntermediateManager,
178 ) -> AccessLayer {
179 AccessLayer {
180 table_dir: table_dir.into(),
181 path_type,
182 object_store,
183 puffin_manager_factory,
184 intermediate_manager,
185 }
186 }
187
188 pub fn table_dir(&self) -> &str {
190 &self.table_dir
191 }
192
193 pub fn object_store(&self) -> &ObjectStore {
195 &self.object_store
196 }
197
198 pub fn path_type(&self) -> PathType {
200 self.path_type
201 }
202
203 pub fn puffin_manager_factory(&self) -> &PuffinManagerFactory {
205 &self.puffin_manager_factory
206 }
207
208 pub fn intermediate_manager(&self) -> &IntermediateManager {
210 &self.intermediate_manager
211 }
212
213 pub(crate) fn build_puffin_manager(&self) -> SstPuffinManager {
215 let store = self.object_store.clone();
216 let path_provider =
217 RegionFilePathFactory::new(self.table_dir().to_string(), self.path_type());
218 self.puffin_manager_factory.build(store, path_provider)
219 }
220
221 pub(crate) async fn delete_index(
222 &self,
223 index_file_id: RegionIndexId,
224 ) -> Result<(), crate::error::Error> {
225 let path = location::index_file_path(
226 &self.table_dir,
227 RegionIndexId::new(index_file_id.file_id, index_file_id.version),
228 self.path_type,
229 );
230 self.object_store
231 .delete(&path)
232 .await
233 .context(DeleteIndexSnafu {
234 file_id: index_file_id.file_id(),
235 })?;
236 Ok(())
237 }
238
239 pub(crate) async fn delete_ssts(
240 &self,
241 region_id: RegionId,
242 file_ids: &[FileId],
243 ) -> Result<(), crate::error::Error> {
244 if file_ids.is_empty() {
245 return Ok(());
246 }
247
248 let attempted_files = file_ids.to_vec();
249 let paths: Vec<_> = file_ids
251 .iter()
252 .map(|file_id| {
253 location::sst_file_path(
254 &self.table_dir,
255 RegionFileId::new(region_id, *file_id),
256 self.path_type,
257 )
258 .trim_start_matches('/')
259 .to_string()
260 })
261 .collect();
262
263 let mut deleter = self
264 .object_store
265 .deleter()
266 .await
267 .with_context(|_| DeleteSstsSnafu {
268 region_id,
269 file_ids: attempted_files.clone(),
270 })?;
271 deleter
272 .delete_iter(paths)
273 .await
274 .with_context(|_| DeleteSstsSnafu {
275 region_id,
276 file_ids: attempted_files.clone(),
277 })?;
278 deleter.close().await.with_context(|_| DeleteSstsSnafu {
279 region_id,
280 file_ids: attempted_files,
281 })?;
282
283 Ok(())
284 }
285
286 pub(crate) async fn delete_indexes(
287 &self,
288 index_ids: &[RegionIndexId],
289 ) -> Result<(), crate::error::Error> {
290 if index_ids.is_empty() {
291 return Ok(());
292 }
293
294 let file_ids: Vec<_> = index_ids
295 .iter()
296 .map(|index_id| index_id.file_id())
297 .collect();
298 let paths: Vec<_> = index_ids
299 .iter()
300 .map(|index_id| {
301 location::index_file_path(&self.table_dir, *index_id, self.path_type)
302 .trim_start_matches('/')
303 .to_string()
304 })
305 .collect();
306
307 let mut deleter = self
308 .object_store
309 .deleter()
310 .await
311 .context(DeleteIndexesSnafu {
312 file_ids: file_ids.clone(),
313 })?;
314 deleter
315 .delete_iter(paths)
316 .await
317 .context(DeleteIndexesSnafu {
318 file_ids: file_ids.clone(),
319 })?;
320 deleter
321 .close()
322 .await
323 .context(DeleteIndexesSnafu { file_ids })?;
324
325 Ok(())
326 }
327
328 pub fn build_region_dir(&self, region_id: RegionId) -> String {
330 region_dir_from_table_dir(&self.table_dir, region_id, self.path_type)
331 }
332
333 pub(crate) fn read_sst(&self, file: FileHandle) -> ParquetReaderBuilder {
335 ParquetReaderBuilder::new(
336 self.table_dir.clone(),
337 self.path_type,
338 file,
339 self.object_store.clone(),
340 )
341 }
342
343 pub async fn write_sst(
347 &self,
348 request: SstWriteRequest,
349 write_opts: &WriteOptions,
350 metrics: &mut Metrics,
351 ) -> Result<SstInfoArray> {
352 self.write_sst_inner(request, write_opts, metrics, None)
353 .await
354 }
355
356 #[cfg(feature = "enterprise")]
362 pub(crate) async fn write_sst_with_guard(
363 &self,
364 request: SstWriteRequest,
365 write_opts: &WriteOptions,
366 metrics: &mut Metrics,
367 context: crate::sst::parquet::writer::SstWriteContext,
368 ) -> Result<SstInfoArray> {
369 self.write_sst_inner(request, write_opts, metrics, Some(context))
370 .await
371 }
372
373 async fn write_sst_inner(
374 &self,
375 request: SstWriteRequest,
376 write_opts: &WriteOptions,
377 metrics: &mut Metrics,
378 context: Option<crate::sst::parquet::writer::SstWriteContext>,
379 ) -> Result<SstInfoArray> {
380 let limits = context.as_ref().map(|context| context.limits);
381 let guard = context.map(|context| context.guard);
382 let op_type = request.op_type;
383 let region_id = request.metadata.region_id;
384 let region_metadata = request.metadata.clone();
385 let cache_manager = request.cache_manager.clone();
386 let override_sequence = if request.preserve_row_sequence {
387 None
388 } else {
389 request.max_sequence
390 };
391
392 let sst_info = if let Some(write_cache) = cache_manager.write_cache()
393 && guard.is_none()
394 {
395 write_cache
397 .write_and_upload_sst(
398 request,
399 SstUploadRequest {
400 dest_path_provider: RegionFilePathFactory::new(
401 self.table_dir.clone(),
402 self.path_type,
403 ),
404 remote_store: self.object_store.clone(),
405 },
406 write_opts,
407 metrics,
408 )
409 .await?
410 } else {
411 let store = self.object_store.clone();
413 let path_provider = RegionFilePathFactory::new(self.table_dir.clone(), self.path_type);
414 let puffin_factory = if limits.is_some() {
415 self.puffin_manager_factory
416 .clone()
417 .with_write_buffer_size(write_opts.write_buffer_size.as_bytes() as usize)
418 } else {
419 self.puffin_manager_factory.clone()
420 };
421 let indexer_builder = IndexerBuilderImpl {
422 build_type: request.op_type.into(),
423 metadata: request.metadata.clone(),
424 puffin_manager: puffin_factory.build(store, path_provider.clone()),
425 write_cache_enabled: false,
426 intermediate_manager: self.intermediate_manager.clone(),
427 index_options: request.index_options,
428 inverted_index_config: request.inverted_index_config,
429 fulltext_index_config: request.fulltext_index_config,
430 bloom_filter_index_config: request.bloom_filter_index_config,
431 };
432 let cleaner = TempFileCleaner::new(region_id, self.object_store.clone());
436 let mut writer = ParquetWriter::new_with_object_store(
437 self.object_store.clone(),
438 request.metadata,
439 request.index_config,
440 indexer_builder,
441 path_provider,
442 metrics,
443 )
444 .await
445 .with_file_cleaner(cleaner)
446 .with_write_concurrency(if guard.is_some() {
447 1
448 } else {
449 crate::sst::DEFAULT_WRITE_CONCURRENCY
450 })
451 .with_write_guard(guard)
452 .with_memory_limits(limits);
453 match request.sst_write_format {
454 FormatType::PrimaryKey => {
455 writer
456 .write_all_flat_as_primary_key(
457 request.source,
458 override_sequence,
459 write_opts,
460 )
461 .await?
462 }
463 FormatType::Flat => {
464 writer
465 .write_all_flat(request.source, override_sequence, write_opts)
466 .await?
467 }
468 }
469 };
470
471 if !sst_info.is_empty() && cache_manager.sst_meta_cache_on_write_enabled() {
473 let runtime = match op_type {
474 OperationType::Compact => common_runtime::compact_runtime(),
475 OperationType::Flush => common_runtime::global_runtime(),
476 };
477 for sst in &sst_info {
478 if let Some(parquet_metadata) = &sst.file_metadata {
479 let file_id = RegionFileId::new(region_id, sst.file_id);
480 let file_path = format!(
481 "region_id={}, file_id={}",
482 file_id.region_id(),
483 file_id.file_id()
484 );
485 let page_index_policy = if parquet_metadata.offset_index().is_some() {
486 PageIndexPolicy::Optional
487 } else {
488 PageIndexPolicy::Skip
489 };
490 let parquet_metadata = parquet_metadata.clone();
491 let region_metadata = region_metadata.clone();
492 let cache_manager = cache_manager.clone();
493 let task_runtime = runtime.clone();
495 runtime.spawn(async move {
496 let preparation = task_runtime.spawn_blocking(move || {
497 prepare_sst_meta_sync(
498 &file_path,
499 Arc::unwrap_or_clone(parquet_metadata),
500 Some(region_metadata),
501 page_index_policy,
502 )
503 }).await.context(crate::error::JoinSnafu).and_then(|result| result);
504 match preparation {
505 Ok(SstMetaPreparation::Prepared(metadata)) => {
506 cache_manager.put_compact_sst_meta(file_id, metadata.compact);
507 cache_manager.put_sst_meta_data(file_id, metadata.decoded).await;
508 }
509 Ok(SstMetaPreparation::DecodedOnly { encoding_error, .. }) => warn!(
510 encoding_error;
511 "Failed to encode parquet metadata for cache, file: {}",
512 file_id.file_id()
513 ),
514 Err(err) => {
515 warn!(err; "Failed to cache parquet metadata for {}", file_id.file_id());
516 }
517 }
518 });
519 }
520 }
521 }
522
523 Ok(sst_info)
524 }
525
526 pub(crate) async fn put_sst(
531 &self,
532 data: &bytes::Bytes,
533 region_id: RegionId,
534 sst_info: &mut SstInfo,
535 cache_manager: &CacheManagerRef,
536 write_buffer_size: ReadableSize,
537 index_build_params: Option<EncodedSstIndexBuildParams>,
538 ) -> Result<Metrics> {
539 let region_file_id = RegionFileId::new(region_id, sst_info.file_id);
540 if let Some(write_cache) = cache_manager.write_cache() {
541 let upload_request = SstUploadRequest {
543 dest_path_provider: RegionFilePathFactory::new(
544 self.table_dir.clone(),
545 self.path_type,
546 ),
547 remote_store: self.object_store.clone(),
548 };
549 write_cache
550 .put_and_upload_sst(
551 data,
552 region_id,
553 sst_info,
554 upload_request,
555 write_buffer_size,
556 index_build_params,
557 )
558 .await
559 } else {
560 let start = Instant::now();
561 let cleaner =
562 TempFileCleaner::new(region_file_id.region_id(), self.object_store.clone());
563 let path_provider = RegionFilePathFactory::new(self.table_dir.clone(), self.path_type);
564 let sst_file_path = path_provider.build_sst_file_path(region_file_id);
565 let mut writer = self
566 .object_store
567 .writer_with(&sst_file_path)
568 .chunk(write_buffer_size.as_bytes() as usize)
569 .concurrent(DEFAULT_WRITE_CONCURRENCY)
570 .await
571 .context(OpenDalSnafu)?;
572 if let Err(err) = writer.write(data.clone()).await.context(OpenDalSnafu) {
573 cleaner.clean_by_file_id(region_file_id.file_id()).await;
574 return Err(err);
575 }
576 if let Err(err) = writer.close().await.context(OpenDalSnafu) {
577 cleaner.clean_by_file_id(region_file_id.file_id()).await;
578 return Err(err);
579 }
580 let mut metrics = Metrics::new(WriteType::Flush);
581 metrics.write_batch = start.elapsed();
582
583 if let Some(params) = index_build_params {
584 let start = Instant::now();
585 let builder = params.indexer_builder(
586 self.puffin_manager_factory
587 .build(self.object_store.clone(), path_provider),
588 false,
589 self.intermediate_manager.clone(),
590 );
591 sst_info.index_metadata =
592 build_index_from_parquet_bytes(&builder, region_file_id, data).await;
593 metrics.update_index = start.elapsed();
594 }
595 Ok(metrics)
596 }
597 }
598
599 pub fn storage_sst_entries(&self) -> impl Stream<Item = Result<StorageSstEntry>> + use<> {
601 let object_store = self.object_store.clone();
602 let table_dir = self.table_dir.clone();
603
604 try_stream! {
605 let mut lister = object_store
606 .lister_with(table_dir.as_str())
607 .recursive(true)
608 .await
609 .context(OpenDalSnafu)?;
610
611 while let Some(entry) = lister.try_next().await.context(OpenDalSnafu)? {
612 let metadata = entry.metadata();
613 if metadata.is_dir() {
614 continue;
615 }
616
617 let path = entry.path();
618 if !path.ends_with(".parquet") && !path.ends_with(".puffin") {
619 continue;
620 }
621
622 let file_size = metadata.content_length();
623 let file_size = if file_size == 0 { None } else { Some(file_size) };
624 let last_modified_ms = metadata
625 .last_modified()
626 .map(|ts| Timestamp::new_millisecond(ts.into_inner().as_millisecond()));
627
628 let entry = StorageSstEntry {
629 file_path: path.to_string(),
630 file_size,
631 last_modified_ms,
632 node_id: None,
633 };
634
635 yield entry;
636 }
637 }
638 }
639}
640
641#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
643pub enum OperationType {
644 Flush,
645 Compact,
646}
647
648pub struct SstWriteRequest {
650 pub op_type: OperationType,
651 pub metadata: RegionMetadataRef,
652 pub source: FlatSource,
653 pub cache_manager: CacheManagerRef,
654 pub max_sequence: Option<SequenceNumber>,
657 pub sst_write_format: FormatType,
658
659 pub preserve_row_sequence: bool,
660
661 pub index_options: IndexOptions,
663 pub index_config: IndexConfig,
664 pub inverted_index_config: InvertedIndexConfig,
665 pub fulltext_index_config: FulltextIndexConfig,
666 pub bloom_filter_index_config: BloomFilterConfig,
667}
668
669pub(crate) struct EncodedSstIndexBuildParams {
671 pub(crate) metadata: RegionMetadataRef,
672 pub(crate) index_options: IndexOptions,
673 pub(crate) inverted_index_config: InvertedIndexConfig,
674 pub(crate) fulltext_index_config: FulltextIndexConfig,
675 pub(crate) bloom_filter_index_config: BloomFilterConfig,
676}
677
678impl EncodedSstIndexBuildParams {
679 pub(crate) fn indexer_builder(
680 self,
681 puffin_manager: SstPuffinManager,
682 write_cache_enabled: bool,
683 intermediate_manager: IntermediateManager,
684 ) -> IndexerBuilderImpl {
685 IndexerBuilderImpl {
686 build_type: OperationType::Flush.into(),
687 metadata: self.metadata,
688 puffin_manager,
689 write_cache_enabled,
690 intermediate_manager,
691 index_options: self.index_options,
692 inverted_index_config: self.inverted_index_config,
693 fulltext_index_config: self.fulltext_index_config,
694 bloom_filter_index_config: self.bloom_filter_index_config,
695 }
696 }
697}
698
699pub(crate) struct TempFileCleaner {
701 region_id: RegionId,
702 object_store: ObjectStore,
703}
704
705impl TempFileCleaner {
706 pub(crate) fn new(region_id: RegionId, object_store: ObjectStore) -> Self {
708 Self {
709 region_id,
710 object_store,
711 }
712 }
713
714 pub(crate) async fn clean_by_file_id(&self, file_id: FileId) {
717 let sst_key = IndexKey::new(self.region_id, file_id, FileType::Parquet).to_string();
718 let index_key = IndexKey::new(self.region_id, file_id, FileType::Puffin(0)).to_string();
719
720 Self::clean_atomic_dir_files(&self.object_store, &[&sst_key, &index_key]).await;
721 }
722
723 pub(crate) async fn clean_atomic_dir_files(
725 local_store: &ObjectStore,
726 names_to_remove: &[&str],
727 ) {
728 let Ok(entries) = local_store.list(ATOMIC_WRITE_DIR).await.inspect_err(|e| {
731 if e.kind() != ErrorKind::NotFound {
732 common_telemetry::error!(e; "Failed to list tmp files for {:?}", names_to_remove)
733 }
734 }) else {
735 return;
736 };
737
738 let actual_files: Vec<_> = entries
741 .into_iter()
742 .filter_map(|entry| {
743 if entry.metadata().is_dir() {
744 return None;
745 }
746
747 let should_remove = names_to_remove
749 .iter()
750 .any(|file| entry.name().starts_with(file));
751 if should_remove {
752 Some(entry.path().to_string())
753 } else {
754 None
755 }
756 })
757 .collect();
758
759 common_telemetry::warn!(
760 "Clean files {:?} under atomic write dir for {:?}",
761 actual_files,
762 names_to_remove
763 );
764
765 if let Err(e) = local_store.delete_iter(actual_files).await {
766 common_telemetry::error!(e; "Failed to delete tmp file for {:?}", names_to_remove);
767 }
768 }
769}
770
771pub(crate) async fn new_fs_cache_store(root: &str) -> Result<ObjectStore> {
772 let atomic_write_dir = Path::new(root).join(ATOMIC_WRITE_DIR);
774 clean_dir(&atomic_write_dir.to_string_lossy()).await?;
775
776 let old_atomic_temp_dir = join_dir(root, OLD_ATOMIC_WRITE_DIR);
778 clean_dir(&old_atomic_temp_dir).await?;
779
780 let builder = Fs::default()
781 .root(root)
782 .atomic_write_dir(&atomic_write_dir.to_string_lossy());
783 let store = ObjectStore::new(builder).context(OpenDalSnafu)?;
784
785 Ok(with_instrument_layers(store, false))
786}
787
788async fn clean_dir(dir: &str) -> Result<()> {
790 if tokio::fs::try_exists(dir)
791 .await
792 .context(CleanDirSnafu { dir })?
793 {
794 tokio::fs::remove_dir_all(dir)
795 .await
796 .context(CleanDirSnafu { dir })?;
797 }
798
799 Ok(())
800}
801
802pub trait FilePathProvider: Send + Sync {
804 fn build_index_file_path(&self, file_id: RegionFileId) -> String;
806
807 fn build_index_file_path_with_version(&self, index_id: RegionIndexId) -> String;
809
810 fn build_sst_file_path(&self, file_id: RegionFileId) -> String;
812}
813
814#[derive(Clone)]
816pub(crate) struct WriteCachePathProvider {
817 file_cache: FileCacheRef,
818}
819
820impl WriteCachePathProvider {
821 pub fn new(file_cache: FileCacheRef) -> Self {
823 Self { file_cache }
824 }
825}
826
827impl FilePathProvider for WriteCachePathProvider {
828 fn build_index_file_path(&self, file_id: RegionFileId) -> String {
829 let puffin_key = IndexKey::new(file_id.region_id(), file_id.file_id(), FileType::Puffin(0));
830 self.file_cache.cache_file_path(puffin_key)
831 }
832
833 fn build_index_file_path_with_version(&self, index_id: RegionIndexId) -> String {
834 let puffin_key = IndexKey::new(
835 index_id.region_id(),
836 index_id.file_id(),
837 FileType::Puffin(index_id.version),
838 );
839 self.file_cache.cache_file_path(puffin_key)
840 }
841
842 fn build_sst_file_path(&self, file_id: RegionFileId) -> String {
843 let parquet_file_key =
844 IndexKey::new(file_id.region_id(), file_id.file_id(), FileType::Parquet);
845 self.file_cache.cache_file_path(parquet_file_key)
846 }
847}
848
849#[derive(Clone, Debug)]
851pub(crate) struct RegionFilePathFactory {
852 pub(crate) table_dir: String,
853 pub(crate) path_type: PathType,
854}
855
856impl RegionFilePathFactory {
857 pub fn new(table_dir: String, path_type: PathType) -> Self {
859 Self {
860 table_dir,
861 path_type,
862 }
863 }
864}
865
866impl FilePathProvider for RegionFilePathFactory {
867 fn build_index_file_path(&self, file_id: RegionFileId) -> String {
868 location::index_file_path_legacy(&self.table_dir, file_id, self.path_type)
869 }
870
871 fn build_index_file_path_with_version(&self, index_id: RegionIndexId) -> String {
872 location::index_file_path(&self.table_dir, index_id, self.path_type)
873 }
874
875 fn build_sst_file_path(&self, file_id: RegionFileId) -> String {
876 location::sst_file_path(&self.table_dir, file_id, self.path_type)
877 }
878}
879
880#[cfg(test)]
881mod tests {
882 use std::path::PathBuf;
883
884 use bytes::Bytes;
885 use common_test_util::temp_dir::create_temp_dir;
886
887 use super::*;
888 use crate::cache::CacheManager;
889 use crate::cache::test_util::new_fs_store;
890 use crate::test_util::TestEnv;
891 use crate::test_util::sst_util::WriteChunkRecorder;
892
893 #[rstest::rstest]
894 #[tokio::test]
895 async fn test_put_sst_uses_configured_buffer_size(
896 #[values(false, true)] enable_write_cache: bool,
897 #[values(1024, 4096)] chunk_size: usize,
898 ) {
899 let mut env = TestEnv::new().await;
900 let remote_chunks = WriteChunkRecorder::default();
901 let remote_store = env.init_object_store_manager().layer(remote_chunks.layer());
902 let local_dir = create_temp_dir("encoded-sst-cache");
903 let local_chunks = WriteChunkRecorder::default();
904 let local_store =
905 new_fs_store(local_dir.path().to_str().unwrap()).layer(local_chunks.layer());
906 let write_cache = if enable_write_cache {
907 Some(
908 env.create_write_cache(local_store.clone(), ReadableSize::mb(10))
909 .await,
910 )
911 } else {
912 None
913 };
914 let cache_manager = Arc::new(
915 CacheManager::builder()
916 .write_cache(write_cache.clone())
917 .build(),
918 );
919 let access_layer = AccessLayer::new(
920 "test",
921 PathType::Bare,
922 remote_store.clone(),
923 env.get_puffin_manager(),
924 env.get_intermediate_manager(),
925 );
926 let file_id = RegionFileId::new(RegionId::new(1024, 1), FileId::random());
927 let encoded = Bytes::from(
929 (0..2 * chunk_size + 17)
930 .map(|i| (i % 251) as u8)
931 .collect::<Vec<_>>(),
932 );
933 access_layer
934 .put_sst(
935 &encoded,
936 file_id.region_id(),
937 &mut SstInfo {
938 file_id: file_id.file_id(),
939 ..Default::default()
940 },
941 &cache_manager,
942 ReadableSize(chunk_size as u64),
943 None,
944 )
945 .await
946 .unwrap();
947
948 let remote_path = location::sst_file_path("test", file_id, PathType::Bare);
949 remote_chunks.assert_chunks(&remote_path, chunk_size, encoded.len());
950 assert_eq!(
951 remote_store.read(&remote_path).await.unwrap().to_bytes(),
952 encoded
953 );
954 if let Some(write_cache) = write_cache {
955 let key = IndexKey::new(file_id.region_id(), file_id.file_id(), FileType::Parquet);
956 let cache_path = write_cache.file_cache().cache_file_path(key);
957 local_chunks.assert_chunks(&cache_path, chunk_size, encoded.len());
958 assert_eq!(
959 local_store.read(&cache_path).await.unwrap().to_bytes(),
960 encoded
961 );
962 }
963 }
964
965 #[rstest::rstest]
966 #[case::flush(OperationType::Flush, false)]
967 #[case::local_compaction(OperationType::Compact, false)]
968 #[case::input_only_compaction(OperationType::Compact, true)]
969 #[tokio::test]
970 async fn test_written_sst_metadata_cache_policy(
971 #[case] op_type: OperationType,
972 #[case] input_only: bool,
973 ) {
974 use crate::cache::CacheStrategy;
975 use crate::sst::file::FileMeta;
976 use crate::sst::file_purger::NoopFilePurger;
977 use crate::sst::parquet::reader::MetadataCacheMetrics;
978 use crate::test_util::sst_util::{
979 new_flat_source_from_record_batches, new_record_batch_by_range, sst_region_metadata,
980 };
981
982 let mut env = TestEnv::new().await;
983 let store = env.init_object_store_manager();
984 let access_layer = AccessLayer::new(
985 "test",
986 PathType::Bare,
987 store,
988 env.get_puffin_manager(),
989 env.get_intermediate_manager(),
990 );
991 let builder = CacheManager::builder().sst_meta_cache_size(1024 * 1024);
992 let cache = Arc::new(if input_only {
993 builder.disable_sst_meta_cache_on_write().build()
994 } else {
995 builder.build()
996 });
997 let metadata = Arc::new(sst_region_metadata());
998 let region_id = metadata.region_id;
999 let infos =
1000 access_layer
1001 .write_sst(
1002 SstWriteRequest {
1003 op_type,
1004 metadata,
1005 source: new_flat_source_from_record_batches(vec![
1006 new_record_batch_by_range(&["a", "b"], 0, 16),
1007 ]),
1008 cache_manager: cache.clone(),
1009 max_sequence: None,
1010 sst_write_format: FormatType::Flat,
1011 preserve_row_sequence: false,
1012 index_options: Default::default(),
1013 index_config: Default::default(),
1014 inverted_index_config: Default::default(),
1015 fulltext_index_config: Default::default(),
1016 bloom_filter_index_config: Default::default(),
1017 },
1018 &WriteOptions::default(),
1019 &mut Metrics::new(WriteType::Compaction),
1020 )
1021 .await
1022 .unwrap();
1023 assert_eq!(1, infos.len());
1024 tokio::time::timeout(Duration::from_secs(10), async {
1027 while Arc::strong_count(&cache) > 1 {
1028 tokio::task::yield_now().await;
1029 }
1030 })
1031 .await
1032 .unwrap();
1033 let info = &infos[0];
1034 let file_id = RegionFileId::new(region_id, info.file_id);
1035 let cached = cache
1036 .get_sst_meta_data(
1037 file_id,
1038 &mut MetadataCacheMetrics::default(),
1039 PageIndexPolicy::Skip,
1040 &common_runtime::compact_runtime(),
1041 )
1042 .await;
1043 assert_eq!(!input_only, cached.is_some());
1044
1045 let file = FileHandle::new(
1046 FileMeta {
1047 region_id,
1048 file_id: info.file_id,
1049 file_size: info.file_size,
1050 ..Default::default()
1051 },
1052 Arc::new(NoopFilePurger),
1053 );
1054 let path = file.file_path(access_layer.table_dir(), access_layer.path_type());
1055 let reader = access_layer
1056 .read_sst(file)
1057 .cache(CacheStrategy::Compaction(cache));
1058 let mut cold = MetadataCacheMetrics::default();
1059 let (first, _) = reader
1060 .read_parquet_metadata(&path, info.file_size, &mut cold, PageIndexPolicy::Skip)
1061 .await
1062 .unwrap();
1063 assert_eq!(input_only, cold.num_reads > 0);
1064 let mut warm = MetadataCacheMetrics::default();
1065 let (second, _) = reader
1066 .read_parquet_metadata(&path, info.file_size, &mut warm, PageIndexPolicy::Skip)
1067 .await
1068 .unwrap();
1069 assert_eq!(16, second.parquet_metadata().file_metadata().num_rows());
1070 assert!(Arc::ptr_eq(&first, &second));
1071 assert_eq!(0, warm.num_reads);
1072 assert_eq!(1, warm.mem_cache_hit);
1073 }
1074
1075 #[tokio::test]
1076 async fn test_new_fs_cache_store() {
1077 let root = common_test_util::temp_dir::create_temp_dir("fs-cache-store");
1078 let dirs = [
1079 root.path().join(ATOMIC_WRITE_DIR),
1080 PathBuf::from(join_dir(
1081 root.path().to_str().unwrap(),
1082 OLD_ATOMIC_WRITE_DIR,
1083 )),
1084 ];
1085 for dir in &dirs {
1086 tokio::fs::create_dir_all(dir).await.unwrap();
1087 tokio::fs::write(dir.join("stale"), b"stale").await.unwrap();
1088 }
1089 let store = new_fs_cache_store(root.path().to_str().unwrap())
1090 .await
1091 .unwrap();
1092 for dir in &dirs {
1093 assert!(!dir.join("stale").exists());
1094 }
1095 store.write("index", "contents").await.unwrap();
1096 assert_eq!(
1097 tokio::fs::read(root.path().join("index")).await.unwrap(),
1098 b"contents"
1099 );
1100 }
1101}