1use std::path::PathBuf;
18use std::sync::Arc;
19use std::time::{Duration, Instant};
20
21use common_base::readable_size::ReadableSize;
22use common_telemetry::{error, info, warn};
23use futures::{FutureExt, TryStreamExt};
24use moka::future::Cache;
25use moka::notification::RemovalCause;
26use moka::policy::EvictionPolicy;
27use object_store::ObjectStore;
28use object_store::util::join_path;
29use snafu::ResultExt;
30
31use crate::error::{OpenDalSnafu, Result};
32use crate::metrics::{CACHE_BYTES, CACHE_HIT, CACHE_MISS};
33
34const MANIFEST_DIR: &str = "cache/object/manifest/";
38
39const MANIFEST_TYPE: &str = "manifest";
41
42const CHUNK_SIZE: usize = 64 * 1024 * 1024; #[derive(Debug, Clone)]
47pub struct ManifestCache {
48 local_store: ObjectStore,
50 index: Cache<String, IndexValue>,
52}
53
54impl ManifestCache {
55 pub async fn new(
57 local_store: ObjectStore,
58 capacity: ReadableSize,
59 ttl: Option<Duration>,
60 recover_sync: bool,
61 ) -> ManifestCache {
62 let total_capacity = capacity.as_bytes();
63
64 info!(
65 "Initializing manifest cache with capacity: {}",
66 ReadableSize(total_capacity)
67 );
68
69 let index = Self::build_cache(local_store.clone(), total_capacity, ttl);
70
71 let cache = ManifestCache { local_store, index };
72
73 cache.recover(recover_sync).await;
75
76 cache
77 }
78
79 fn build_cache(
81 local_store: ObjectStore,
82 capacity: u64,
83 ttl: Option<Duration>,
84 ) -> Cache<String, IndexValue> {
85 let cache_store = local_store;
86 let mut builder = Cache::builder()
87 .eviction_policy(EvictionPolicy::lru())
88 .weigher(|key: &String, value: &IndexValue| -> u32 {
89 key.len() as u32 + value.file_size
90 })
91 .max_capacity(capacity)
92 .async_eviction_listener(move |key: Arc<String>, value: IndexValue, cause| {
93 let store = cache_store.clone();
94 let file_path = join_path(MANIFEST_DIR, &key);
96 async move {
97 if let RemovalCause::Replaced = cause {
98 CACHE_BYTES
101 .with_label_values(&[MANIFEST_TYPE])
102 .sub(value.file_size.into());
103 return;
104 }
105
106 match store.delete(&file_path).await {
107 Ok(()) => {
108 CACHE_BYTES
109 .with_label_values(&[MANIFEST_TYPE])
110 .sub(value.file_size.into());
111 }
112 Err(e) => {
113 warn!(e; "Failed to delete cached manifest file {}", file_path);
114 }
115 }
116 }
117 .boxed()
118 });
119 if let Some(ttl) = ttl {
120 builder = builder.time_to_idle(ttl);
121 }
122 builder.build()
123 }
124
125 pub(crate) async fn put(&self, key: String, value: IndexValue) {
129 CACHE_BYTES
130 .with_label_values(&[MANIFEST_TYPE])
131 .add(value.file_size.into());
132 self.index.insert(key, value).await;
133
134 self.index.run_pending_tasks().await;
136 }
137
138 pub(crate) async fn get(&self, key: &str) -> Option<IndexValue> {
140 self.index.get(key).await
141 }
142
143 pub(crate) async fn remove(&self, key: &str) {
145 let file_path = self.cache_file_path(key);
146 self.index.remove(key).await;
147 if let Err(e) = self.local_store.delete(&file_path).await {
149 warn!(e; "Failed to delete a cached manifest file {}", file_path);
150 }
151 }
152
153 pub(crate) async fn remove_batch(&self, keys: &[String]) {
155 if keys.is_empty() {
156 return;
157 }
158
159 for key in keys {
160 self.index.remove(key).await;
161 }
162
163 let file_paths: Vec<String> = keys.iter().map(|key| self.cache_file_path(key)).collect();
164
165 if let Err(e) = self.local_store.delete_iter(file_paths).await {
166 warn!(e; "Failed to delete cached manifest files in batch");
167 }
168 }
169
170 async fn recover_inner(&self) -> Result<()> {
171 let now = Instant::now();
172 let mut lister = self
173 .local_store
174 .lister_with(MANIFEST_DIR)
175 .recursive(true)
176 .await
177 .context(OpenDalSnafu)?;
178 let (mut total_size, mut total_keys) = (0i64, 0);
179 while let Some(entry) = lister.try_next().await.context(OpenDalSnafu)? {
180 let meta = entry.metadata();
181 if !meta.is_file() {
182 continue;
183 }
184
185 let meta = self
186 .local_store
187 .stat(entry.path())
188 .await
189 .context(OpenDalSnafu)?;
190 let file_size = meta.content_length() as u32;
191 let key = entry.path().trim_start_matches(MANIFEST_DIR).to_string();
192 common_telemetry::debug!("Manifest cache recover {}, size: {}", key, file_size);
193 self.index.insert(key, IndexValue { file_size }).await;
194 let size = i64::from(file_size);
195 total_size += size;
196 total_keys += 1;
197 }
198 CACHE_BYTES
199 .with_label_values(&[MANIFEST_TYPE])
200 .add(total_size);
201
202 self.index.run_pending_tasks().await;
205
206 let weight = self.index.weighted_size();
207 let count = self.index.entry_count();
208 info!(
209 "Recovered manifest cache, num_keys: {}, num_bytes: {}, count: {}, weight: {}, cost: {:?}",
210 total_keys,
211 total_size,
212 count,
213 weight,
214 now.elapsed()
215 );
216 Ok(())
217 }
218
219 pub(crate) async fn recover(&self, sync: bool) {
221 let moved_self = self.clone();
222 let handle = tokio::spawn(async move {
223 if let Err(err) = moved_self.recover_inner().await {
224 error!(err; "Failed to recover manifest cache.")
225 }
226
227 moved_self.clean_empty_dirs(true).await;
228 });
229
230 if sync {
231 let _ = handle.await;
232 }
233 }
234
235 pub(crate) fn cache_file_path(&self, key: &str) -> String {
237 join_path(MANIFEST_DIR, key)
238 }
239
240 pub(crate) async fn get_file(&self, key: &str) -> Option<Vec<u8>> {
243 if self.get(key).await.is_none() {
244 CACHE_MISS.with_label_values(&[MANIFEST_TYPE]).inc();
245 return None;
246 }
247
248 let cache_file_path = self.cache_file_path(key);
249 match self.local_store.read(&cache_file_path).await {
250 Ok(data) => {
251 CACHE_HIT.with_label_values(&[MANIFEST_TYPE]).inc();
252 Some(data.to_vec())
253 }
254 Err(e) => {
255 warn!(e; "Failed to read cached manifest file {}", cache_file_path);
256 CACHE_MISS.with_label_values(&[MANIFEST_TYPE]).inc();
257 None
258 }
259 }
260 }
261
262 pub(crate) async fn put_file(&self, key: String, data: Vec<u8>) {
264 let cache_file_path = self.cache_file_path(&key);
265
266 if let Err(e) = self
267 .local_store
268 .write_with(&cache_file_path, data.clone())
269 .chunk(CHUNK_SIZE)
270 .await
271 {
272 warn!(e; "Failed to write manifest to cache {}", cache_file_path);
273 return;
274 }
275
276 let file_size = data.len() as u32;
277 self.put(key, IndexValue { file_size }).await;
278 }
279
280 pub(crate) async fn clean_empty_dirs(&self, check_mtime: bool) {
285 info!("Clean empty dirs start");
286
287 let root = self.local_store.info().root();
288 let manifest_dir = PathBuf::from(root).join(MANIFEST_DIR);
289 let manifest_dir_clone = manifest_dir.clone();
290
291 let result = tokio::task::spawn_blocking(move || {
292 Self::clean_empty_dirs_sync(&manifest_dir_clone, check_mtime)
293 })
294 .await;
295
296 match result {
297 Ok(Ok(())) => {
298 info!("Clean empty dirs end");
299 }
300 Ok(Err(e)) => {
301 warn!(e; "Failed to clean empty directories under {}", manifest_dir.display());
302 }
303 Err(e) => {
304 warn!(e; "Failed to spawn blocking task for cleaning empty directories");
305 }
306 }
307 }
308
309 pub(crate) async fn clean_manifests(&self, dir: &str) {
311 info!("Clean manifest cache for directory: {}", dir);
312
313 let cache_dir = join_path(MANIFEST_DIR, dir);
314 let mut lister = match self
315 .local_store
316 .lister_with(&cache_dir)
317 .recursive(true)
318 .await
319 {
320 Ok(lister) => lister,
321 Err(e) => {
322 warn!(e; "Failed to list manifest files under {}", cache_dir);
323 return;
324 }
325 };
326
327 let mut keys_to_remove = Vec::new();
328 loop {
329 match lister.try_next().await {
330 Ok(Some(entry)) => {
331 let meta = entry.metadata();
332 if meta.is_file() {
333 keys_to_remove
334 .push(entry.path().trim_start_matches(MANIFEST_DIR).to_string());
335 }
336 }
337 Ok(None) => break,
338 Err(e) => {
339 warn!(e; "Failed to read entry while listing {}", cache_dir);
340 break;
341 }
342 }
343 }
344
345 info!(
346 "Going to remove files from manifest cache, files: {:?}",
347 keys_to_remove
348 );
349
350 self.remove_batch(&keys_to_remove).await;
352
353 let root = self.local_store.info().root();
355 let dir_path = PathBuf::from(root).join(&cache_dir);
356 let dir_path_clone = dir_path.clone();
357
358 let result = tokio::task::spawn_blocking(move || {
359 Self::clean_empty_dirs_sync(&dir_path_clone, false)
360 })
361 .await;
362
363 match result {
364 Ok(Ok(())) => {
365 info!("Cleaned manifest cache for directory: {}", dir);
366 }
367 Ok(Err(e)) => {
368 warn!(e; "Failed to clean empty directories under {}", dir_path.display());
369 }
370 Err(e) => {
371 warn!(e; "Failed to spawn blocking task for cleaning empty directories");
372 }
373 }
374 }
375
376 fn clean_empty_dirs_sync(dir: &PathBuf, check_mtime: bool) -> std::io::Result<()> {
381 let is_empty = Self::remove_empty_dirs_recursive_sync(dir, check_mtime)?;
382 if is_empty {
383 if let Err(e) = std::fs::remove_dir(dir) {
384 if e.kind() != std::io::ErrorKind::NotFound {
385 warn!(e; "Failed to remove empty root dir {}", dir.display());
386 return Err(e);
387 } else {
388 info!("Empty root dir not found before removal {}", dir.display());
389 }
390 } else {
391 info!(
392 "Removed empty root dir {} from manifest cache",
393 dir.display()
394 );
395 }
396 }
397 Ok(())
398 }
399
400 fn remove_empty_dirs_recursive_sync(dir: &PathBuf, check_mtime: bool) -> std::io::Result<bool> {
401 common_telemetry::debug!(
402 "Maybe remove empty dir: {:?}, check_mtime: {}",
403 dir,
404 check_mtime
405 );
406 let entries = match std::fs::read_dir(dir) {
407 Ok(entries) => entries,
408 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
409 return Ok(true);
411 }
412 Err(e) => return Err(e),
413 };
414
415 let mut is_empty = true;
416 for entry in entries {
419 let entry = entry?;
420 let path = entry.path();
421 let metadata = std::fs::metadata(&path)?;
422
423 if metadata.is_dir() {
424 if check_mtime
426 && let Ok(modified) = metadata.modified()
427 && let Ok(elapsed) = modified.elapsed()
428 && elapsed < Duration::from_secs(3600)
429 {
430 common_telemetry::debug!("Skip directory by mtime, elapsed: {:?}", elapsed);
431 is_empty = false;
433 continue;
434 }
435
436 let subdir_empty = Self::remove_empty_dirs_recursive_sync(&path, check_mtime)?;
437 if subdir_empty {
438 if let Err(e) = std::fs::remove_dir(&path) {
439 if e.kind() != std::io::ErrorKind::NotFound {
440 warn!(e; "Failed to remove empty directory {}", path.display());
441 is_empty = false;
442 } else {
443 info!(
444 "Empty directory {} not found before removal",
445 path.display()
446 );
447 }
448 } else {
449 info!(
450 "Removed empty directory {} from manifest cache",
451 path.display()
452 );
453 }
454 } else {
455 is_empty = false;
456 }
457 } else {
458 is_empty = false;
459 }
460 }
461
462 Ok(is_empty)
463 }
464}
465
466#[derive(Debug, Clone)]
470pub(crate) struct IndexValue {
471 pub(crate) file_size: u32,
473}
474
475#[cfg(test)]
476mod tests {
477 use common_test_util::temp_dir::create_temp_dir;
478 use object_store::services::Fs;
479
480 use super::*;
481
482 fn new_fs_store(path: &str) -> ObjectStore {
483 let builder = Fs::default().root(path);
484 ObjectStore::new(builder).unwrap().finish()
485 }
486
487 #[tokio::test]
488 async fn test_manifest_cache_basic() {
489 common_telemetry::init_default_ut_logging();
490
491 let dir = create_temp_dir("");
492 let local_store = new_fs_store(dir.path().to_str().unwrap());
493
494 let cache = ManifestCache::new(local_store.clone(), ReadableSize::mb(10), None, true).await;
495 let key = "region_1/manifest/00000000000000000007.json";
496 let file_path = cache.cache_file_path(key);
497
498 assert!(cache.get(key).await.is_none());
500
501 local_store
503 .write(&file_path, b"manifest content".as_slice())
504 .await
505 .unwrap();
506 cache
508 .put(key.to_string(), IndexValue { file_size: 16 })
509 .await;
510
511 let value = cache.get(key).await.unwrap();
513 assert_eq!(16, value.file_size);
514
515 cache.index.run_pending_tasks().await;
517 assert_eq!(59, cache.index.weighted_size());
518
519 cache.remove(key).await;
521 cache.index.run_pending_tasks().await;
522 assert!(cache.get(key).await.is_none());
523
524 cache.index.run_pending_tasks().await;
526
527 assert!(!local_store.exists(&file_path).await.unwrap());
529 assert_eq!(0, cache.index.weighted_size());
530 }
531
532 #[tokio::test]
533 async fn test_manifest_cache_recover() {
534 common_telemetry::init_default_ut_logging();
535
536 let dir = create_temp_dir("");
537 let local_store = new_fs_store(dir.path().to_str().unwrap());
538 let cache = ManifestCache::new(local_store.clone(), ReadableSize::mb(10), None, true).await;
539
540 let keys = [
542 "region_1/manifest/00000000000000000001.json",
543 "region_1/manifest/00000000000000000002.json",
544 "region_1/manifest/00000000000000000001.checkpoint",
545 "region_2/manifest/00000000000000000001.json",
546 ];
547
548 let mut total_size = 0;
549 for (i, key) in keys.iter().enumerate() {
550 let file_path = cache.cache_file_path(key);
551 let content = format!("manifest-{}", i).into_bytes();
552 local_store
553 .write(&file_path, content.clone())
554 .await
555 .unwrap();
556
557 cache
559 .put(
560 key.to_string(),
561 IndexValue {
562 file_size: content.len() as u32,
563 },
564 )
565 .await;
566 total_size += content.len() + key.len();
567 }
568
569 let cache = ManifestCache::new(local_store.clone(), ReadableSize::mb(10), None, true).await;
571
572 cache.index.run_pending_tasks().await;
574 let total_cached = cache.index.weighted_size() as usize;
575 assert_eq!(total_size, total_cached);
576
577 for (i, key) in keys.iter().enumerate() {
579 let value = cache.get(key).await.unwrap();
580 assert_eq!(format!("manifest-{}", i).len() as u32, value.file_size);
581 }
582 }
583
584 #[tokio::test]
585 async fn test_cache_file_path() {
586 let dir = create_temp_dir("");
587 let local_store = new_fs_store(dir.path().to_str().unwrap());
588 let cache = ManifestCache::new(local_store, ReadableSize::mb(10), None, true).await;
589
590 assert_eq!(
591 "cache/object/manifest/region_1/manifest/00000000000000000007.json",
592 cache.cache_file_path("region_1/manifest/00000000000000000007.json")
593 );
594 assert_eq!(
595 "cache/object/manifest/region_1/manifest/00000000000000000007.checkpoint",
596 cache.cache_file_path("region_1/manifest/00000000000000000007.checkpoint")
597 );
598 }
599
600 #[tokio::test]
601 async fn test_clean_empty_dirs_sync_no_mtime_check() {
602 common_telemetry::init_default_ut_logging();
603
604 let dir = create_temp_dir("");
605 let root = PathBuf::from(dir.path());
606
607 let empty_dir1 = root.join("empty_dir1");
620 let empty_dir2 = root.join("empty_dir2");
621 let empty_subdir = empty_dir2.join("empty_subdir");
622 let non_empty_dir = root.join("non_empty_dir");
623 let nested = root.join("nested");
624 let nested_empty = nested.join("empty_subdir1");
625 let nested_non_empty = nested.join("non_empty_subdir");
626
627 std::fs::create_dir_all(&empty_dir1).unwrap();
629 std::fs::create_dir_all(&empty_subdir).unwrap();
630 std::fs::create_dir_all(&non_empty_dir).unwrap();
631 std::fs::create_dir_all(&nested_empty).unwrap();
632 std::fs::create_dir_all(&nested_non_empty).unwrap();
633
634 std::fs::write(non_empty_dir.join("file.txt"), b"content").unwrap();
636 std::fs::write(nested_non_empty.join("file.txt"), b"content").unwrap();
637
638 assert!(empty_dir1.exists());
640 assert!(empty_dir2.exists());
641 assert!(empty_subdir.exists());
642 assert!(non_empty_dir.exists());
643 assert!(nested.exists());
644 assert!(nested_empty.exists());
645 assert!(nested_non_empty.exists());
646
647 ManifestCache::clean_empty_dirs_sync(&root, false).unwrap();
649
650 assert!(!empty_dir1.exists());
652 assert!(!empty_dir2.exists());
653 assert!(!empty_subdir.exists());
654 assert!(!nested_empty.exists());
655
656 assert!(non_empty_dir.exists());
658 assert!(non_empty_dir.join("file.txt").exists());
659 assert!(nested.exists());
660 assert!(nested_non_empty.exists());
661 assert!(nested_non_empty.join("file.txt").exists());
662 }
663
664 #[tokio::test]
665 async fn test_clean_empty_dirs_sync_with_mtime_check() {
666 common_telemetry::init_default_ut_logging();
667
668 let dir = create_temp_dir("");
669 let root = PathBuf::from(dir.path());
670
671 let empty_dir1 = root.join("empty_dir1");
680 let empty_dir2 = root.join("empty_dir2");
681 let empty_subdir = empty_dir2.join("empty_subdir");
682 let non_empty_dir = root.join("non_empty_dir");
683
684 std::fs::create_dir_all(&empty_dir1).unwrap();
686 std::fs::create_dir_all(&empty_subdir).unwrap();
687 std::fs::create_dir_all(&non_empty_dir).unwrap();
688
689 std::fs::write(non_empty_dir.join("file.txt"), b"content").unwrap();
691
692 assert!(empty_dir1.exists());
694 assert!(empty_dir2.exists());
695 assert!(empty_subdir.exists());
696 assert!(non_empty_dir.exists());
697
698 ManifestCache::clean_empty_dirs_sync(&root, true).unwrap();
701
702 assert!(empty_dir1.exists());
704 assert!(empty_dir2.exists());
705 assert!(empty_subdir.exists());
706
707 assert!(non_empty_dir.exists());
709 assert!(non_empty_dir.join("file.txt").exists());
710 }
711}