1use std::{
5 fs, io,
6 path::{Path, PathBuf},
7 sync::{
8 Arc, Mutex, Once,
9 atomic::{AtomicBool, Ordering},
10 },
11 thread::sleep,
12};
13
14use dashmap::{DashMap, DashSet};
15use lru_mem::{LruCache, entry_size};
16use threadpool::ThreadPool;
17
18use crate::{
19 hash::ObjectHash,
20 internal::pack::cache_object::{ArcWrapper, CacheObject, FileLoadStore, MemSizeRecorder},
21 time_it,
22};
23
24const CACHE_LAYOUT_VERSION: &str = "rkyv-v1";
28
29pub trait _Cache {
33 fn new(mem_size: Option<usize>, tmp_path: PathBuf, thread_num: usize) -> Self
34 where
35 Self: Sized;
36 fn get_hash(&self, offset: usize) -> Option<ObjectHash>;
37 fn insert(&self, offset: usize, hash: ObjectHash, obj: CacheObject) -> Arc<CacheObject>;
38 fn get_by_offset(&self, offset: usize) -> Option<Arc<CacheObject>>;
39 fn get_by_hash(&self, h: ObjectHash) -> Option<Arc<CacheObject>>;
40 fn total_inserted(&self) -> usize;
41 fn memory_used(&self) -> usize;
42 fn clear(&self);
43}
44
45impl lru_mem::HeapSize for ObjectHash {
46 fn heap_size(&self) -> usize {
47 0
48 }
49}
50
51pub struct Caches {
55 map_offset: DashMap<usize, ObjectHash>, hash_set: DashSet<ObjectHash>, resident_hash_set: DashSet<ObjectHash>, lru_cache: Mutex<LruCache<ObjectHash, ArcWrapper<CacheObject>>>,
63 unbounded_cache: Option<DashMap<ObjectHash, Arc<CacheObject>>>,
64 unbounded_offset_cache: Option<DashMap<usize, Arc<CacheObject>>>,
65 mem_size: Option<usize>,
66 tmp_path: PathBuf,
67 path_prefixes: [Once; 256],
68 pool: Arc<ThreadPool>,
69 complete_signal: Arc<AtomicBool>,
70}
71
72impl Caches {
73 fn try_get(&self, hash: ObjectHash) -> Option<Arc<CacheObject>> {
75 let mut map = self.lru_cache.lock().unwrap();
76 map.get(&hash).map(|x| x.data.clone())
77 }
78
79 fn insert_lru_resident(
80 &self,
81 map: &mut LruCache<ObjectHash, ArcWrapper<CacheObject>>,
82 hash: ObjectHash,
83 obj: ArcWrapper<CacheObject>,
84 ) {
85 let size = entry_size(&hash, &obj);
86 if size <= map.max_size() {
87 while map.current_size() + size > map.max_size() {
88 if let Some((evicted_hash, _)) = map.remove_lru() {
89 self.resident_hash_set.remove(&evicted_hash);
90 } else {
91 break;
92 }
93 }
94 }
95
96 if map.insert(hash, obj).is_ok() {
97 self.resident_hash_set.insert(hash);
98 } else {
99 self.resident_hash_set.remove(&hash);
100 }
101 }
102
103 fn get_fallback(&self, hash: ObjectHash) -> io::Result<Arc<CacheObject>> {
106 let path = self.generate_temp_path(&self.tmp_path, hash);
107 let obj = {
109 loop {
110 match Self::read_from_temp(&path) {
111 Ok(x) => break x,
112 Err(e) if e.kind() == io::ErrorKind::NotFound => {
113 sleep(std::time::Duration::from_millis(10));
114 continue;
115 }
116 Err(e) => return Err(e), }
118 }
119 };
120
121 let mut map = self.lru_cache.lock().unwrap();
122 let obj = Arc::new(obj);
123 let mut x = ArcWrapper::new(
124 obj.clone(),
125 self.complete_signal.clone(),
126 Some(self.pool.clone()),
127 );
128 x.set_store_path(path);
129 self.insert_lru_resident(&mut map, hash, x);
130 Ok(obj)
131 }
132
133 fn generate_temp_path(&self, tmp_path: &Path, hash: ObjectHash) -> PathBuf {
135 let mut path =
137 PathBuf::with_capacity(self.tmp_path.capacity() + hash.to_string().len() + 5);
138 path.push(tmp_path);
139 path.push(CACHE_LAYOUT_VERSION);
140 let hash_str = hash._to_string();
141 path.push(&hash_str[..2]); self.path_prefixes[hash.as_ref()[0] as usize].call_once(|| {
143 if !path.exists() {
145 fs::create_dir_all(&path).unwrap();
146 }
147 });
148 path.push(hash_str);
149 path
150 }
151
152 fn read_from_temp(path: &Path) -> io::Result<CacheObject> {
154 let obj = CacheObject::f_load(path)?;
155 obj.record_mem_size();
158 Ok(obj)
159 }
160
161 pub fn queued_tasks(&self) -> usize {
163 self.pool.queued_count()
164 }
165
166 pub(crate) fn is_unbounded(&self) -> bool {
167 self.mem_size.is_none()
168 }
169
170 pub fn memory_used_index(&self) -> usize {
172 let hash_cache_size = if let Some(cache) = &self.unbounded_cache {
173 cache.capacity()
174 * (std::mem::size_of::<ObjectHash>() + std::mem::size_of::<Arc<CacheObject>>())
175 } else {
176 self.hash_set.capacity() * std::mem::size_of::<ObjectHash>()
177 };
178 let offset_cache_size = if let Some(cache) = &self.unbounded_offset_cache {
179 cache.capacity()
180 * (std::mem::size_of::<usize>() + std::mem::size_of::<Arc<CacheObject>>())
181 } else {
182 self.map_offset.capacity()
183 * (std::mem::size_of::<usize>() + std::mem::size_of::<ObjectHash>())
184 };
185 hash_cache_size + offset_cache_size
186 }
187
188 pub(crate) fn shutdown(&self) {
189 time_it!("Caches clear", {
190 self.complete_signal.store(true, Ordering::Release);
191 self.pool.join();
192 self.lru_cache
193 .lock()
194 .unwrap_or_else(|e| e.into_inner())
195 .clear();
196 if let Some(cache) = &self.unbounded_cache {
197 cache.clear();
198 cache.shrink_to_fit();
199 }
200 if let Some(cache) = &self.unbounded_offset_cache {
201 cache.clear();
202 cache.shrink_to_fit();
203 }
204 self.hash_set.clear();
205 self.hash_set.shrink_to_fit();
206 self.resident_hash_set.clear();
207 self.resident_hash_set.shrink_to_fit();
208 self.map_offset.clear();
209 self.map_offset.shrink_to_fit();
210 });
211 }
212
213 pub fn remove_tmp_dir(&self) -> io::Result<()> {
215 time_it!("Remove tmp dir", {
216 if self.tmp_path.exists() {
217 match fs::remove_dir_all(&self.tmp_path) {
218 Ok(()) => {}
219 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
220 Err(e) => return Err(e),
221 }
222
223 if let Some(parent) = self.tmp_path.parent() {
224 let is_cache_temp = parent
225 .file_name()
226 .and_then(|n| n.to_str())
227 .map(|n| n == ".cache_temp")
228 .unwrap_or(false);
229 if is_cache_temp {
230 match fs::remove_dir(parent) {
231 Ok(()) => {}
232 Err(e)
233 if matches!(
234 e.kind(),
235 io::ErrorKind::DirectoryNotEmpty | io::ErrorKind::NotFound
236 ) => {}
237 Err(e) => return Err(e),
238 }
239 }
240 }
241 }
242
243 Ok(())
244 })
245 }
246
247 pub fn remove_unbounded(&self, offset: usize, hash: ObjectHash) {
248 if self.unbounded_cache.is_some() && self.lru_cache.lock().unwrap().remove(&hash).is_some()
249 {
250 self.resident_hash_set.remove(&hash);
251 }
252 if let Some(cache) = &self.unbounded_offset_cache {
253 cache.remove(&offset);
254 }
255 if let Some(cache) = &self.unbounded_cache {
256 cache.remove(&hash);
257 }
258 }
259}
260
261impl _Cache for Caches {
262 fn new(mem_size: Option<usize>, tmp_path: PathBuf, thread_num: usize) -> Self
265 where
266 Self: Sized,
267 {
268 if mem_size.is_some() {
270 fs::create_dir_all(&tmp_path).unwrap();
271 }
272
273 Caches {
274 map_offset: DashMap::new(),
275 hash_set: DashSet::new(),
276 resident_hash_set: DashSet::new(),
277 lru_cache: Mutex::new(LruCache::new(mem_size.unwrap_or(usize::MAX))),
278 unbounded_cache: mem_size.is_none().then(DashMap::new),
279 unbounded_offset_cache: mem_size.is_none().then(DashMap::new),
280 mem_size,
281 tmp_path,
282 path_prefixes: [const { Once::new() }; 256],
283 pool: Arc::new(ThreadPool::new(thread_num)),
284 complete_signal: Arc::new(AtomicBool::new(false)),
285 }
286 }
287
288 fn get_hash(&self, offset: usize) -> Option<ObjectHash> {
289 if let Some(cache) = &self.unbounded_offset_cache {
290 return cache.get(&offset).and_then(|obj| obj.base_object_hash());
291 }
292 self.map_offset.get(&offset).map(|x| *x)
293 }
294
295 fn insert(&self, offset: usize, hash: ObjectHash, obj: CacheObject) -> Arc<CacheObject> {
296 let obj_arc = Arc::new(obj);
297 if let Some(cache) = &self.unbounded_cache {
298 cache.insert(hash, obj_arc.clone());
299 if let Some(offset_cache) = &self.unbounded_offset_cache {
300 offset_cache.insert(offset, obj_arc.clone());
301 }
302 let mut map = self.lru_cache.lock().unwrap();
303 let a_obj = ArcWrapper::new(
304 obj_arc.clone(),
305 self.complete_signal.clone(),
306 Some(self.pool.clone()),
307 );
308 self.insert_lru_resident(&mut map, hash, a_obj);
309 } else {
310 let mut map = self.lru_cache.lock().unwrap();
312 let mut a_obj = ArcWrapper::new(
313 obj_arc.clone(),
314 self.complete_signal.clone(),
315 Some(self.pool.clone()),
316 );
317 if self.mem_size.is_some() {
318 a_obj.set_store_path(self.generate_temp_path(&self.tmp_path, hash));
319 }
320 self.insert_lru_resident(&mut map, hash, a_obj);
321 self.hash_set.insert(hash);
322 self.map_offset.insert(offset, hash);
324 }
325
326 obj_arc
327 }
328
329 fn get_by_offset(&self, offset: usize) -> Option<Arc<CacheObject>> {
331 if let Some(cache) = &self.unbounded_offset_cache {
332 return cache
333 .get(&offset)
334 .and_then(|obj| obj.base_object_hash())
335 .and_then(|hash| self.try_get(hash));
336 }
337
338 match self.map_offset.get(&offset) {
339 Some(x) => self.get_by_hash(*x),
340 None => None,
341 }
342 }
343
344 fn get_by_hash(&self, hash: ObjectHash) -> Option<Arc<CacheObject>> {
346 if self.mem_size.is_none() {
347 if let Some(cache) = &self.unbounded_cache
348 && !cache.contains_key(&hash)
349 {
350 return None;
351 }
352 return self.try_get(hash);
353 }
354
355 if self.hash_set.contains(&hash) {
357 if !self.resident_hash_set.contains(&hash) {
358 return self.get_fallback(hash).ok();
359 }
360 match self.try_get(hash) {
361 Some(x) => Some(x),
362 None => {
363 if self.mem_size.is_none() {
364 panic!("should not be here when mem_size is not set")
365 }
366 self.get_fallback(hash).ok()
367 }
368 }
369 } else {
370 None
371 }
372 }
373
374 fn total_inserted(&self) -> usize {
375 if self.mem_size.is_some() {
376 self.hash_set.len()
377 } else if let Some(cache) = &self.unbounded_offset_cache {
378 cache.len()
379 } else {
380 self.map_offset.len()
381 }
382 }
383 fn memory_used(&self) -> usize {
384 self.lru_cache.lock().unwrap().current_size() + self.memory_used_index()
385 }
386 fn clear(&self) {
387 self.shutdown();
388
389 assert_eq!(self.pool.queued_count(), 0);
390 assert_eq!(self.pool.active_count(), 0);
391 assert_eq!(self.lru_cache.lock().unwrap().len(), 0);
392 }
393}
394
395#[cfg(test)]
396mod test {
397 use std::{env, sync::Arc, thread};
398
399 use super::*;
400 use crate::{
401 hash::{HashKind, ObjectHash, set_hash_kind_for_test},
402 internal::{object::types::ObjectType, pack::cache_object::CacheObjectInfo},
403 };
404
405 fn make_obj(size: usize, hash: ObjectHash) -> CacheObject {
407 CacheObject {
408 info: CacheObjectInfo::BaseObject(ObjectType::Blob, hash),
409 data_decompressed: vec![0; size],
410 mem_recorder: None,
411 offset: 0,
412 crc32: 0,
413 is_delta_in_pack: false,
414 known_hash: None,
415 }
416 }
417
418 #[test]
420 fn test_cache_single_thread() {
421 for (kind, cap, size_ab, size_c, tmp_dir) in [
422 (
423 HashKind::Sha1,
424 2048usize,
425 800usize,
426 1700usize,
427 "tests/.cache_tmp",
428 ),
429 (
430 HashKind::Sha256,
431 4096usize,
432 1500usize,
433 3000usize,
434 "tests/.cache_tmp_sha256",
435 ),
436 ] {
437 let _guard = set_hash_kind_for_test(kind);
438 let source = PathBuf::from(env::current_dir().unwrap().parent().unwrap());
439 let tmp_path = source.clone().join(tmp_dir);
440 if tmp_path.exists() {
441 fs::remove_dir_all(&tmp_path).unwrap();
442 }
443
444 let cache = Caches::new(Some(cap), tmp_path, 1);
445 let a_hash = ObjectHash::new(String::from("a").as_bytes());
446 let b_hash = ObjectHash::new(String::from("b").as_bytes());
447 let c_hash = ObjectHash::new(String::from("c").as_bytes());
448
449 let a = make_obj(size_ab, a_hash);
450 let b = make_obj(size_ab, b_hash);
451 let c = make_obj(size_c, c_hash);
452
453 cache.insert(a.offset, a_hash, a.clone());
455 assert!(cache.hash_set.contains(&a_hash));
456 assert!(cache.try_get(a_hash).is_some());
457
458 cache.insert(b.offset, b_hash, b.clone());
460 assert!(cache.hash_set.contains(&b_hash));
461 assert!(cache.try_get(b_hash).is_some());
462 assert!(cache.try_get(a_hash).is_some());
463
464 cache.insert(c.offset, c_hash, c.clone());
466 assert!(cache.try_get(a_hash).is_none());
467 assert!(cache.try_get(b_hash).is_none());
468 assert!(cache.try_get(c_hash).is_some());
469 assert!(cache.get_by_hash(c_hash).is_some());
470 }
471 }
472
473 #[test]
475 fn test_cache_multi_thread_mixed_hash_kinds() {
476 let base = PathBuf::from(env::current_dir().unwrap().parent().unwrap());
477 let tmp_path = base.join("tests/.cache_tmp_mixed");
478 if tmp_path.exists() {
479 fs::remove_dir_all(&tmp_path).unwrap();
480 }
481
482 let cache = Arc::new(Caches::new(Some(4096), tmp_path, 2));
483
484 let cache_sha1 = Arc::clone(&cache);
485 let handle_sha1 = thread::spawn(move || {
486 let _g = set_hash_kind_for_test(HashKind::Sha1);
487 let hash = ObjectHash::new(b"sha1-entry");
488 let obj = CacheObject {
489 info: CacheObjectInfo::BaseObject(ObjectType::Blob, hash),
490 data_decompressed: vec![0; 800],
491 mem_recorder: None,
492 offset: 1,
493 crc32: 0,
494 is_delta_in_pack: false,
495 known_hash: None,
496 };
497 cache_sha1.insert(obj.offset, hash, obj.clone());
498 assert!(cache_sha1.hash_set.contains(&hash));
499 assert!(cache_sha1.try_get(hash).is_some());
500 });
501
502 let cache_sha256 = Arc::clone(&cache);
503 let handle_sha256 = thread::spawn(move || {
504 let _g = set_hash_kind_for_test(HashKind::Sha256);
505 let hash = ObjectHash::new(b"sha256-entry");
506 let obj = CacheObject {
507 info: CacheObjectInfo::BaseObject(ObjectType::Blob, hash),
508 data_decompressed: vec![0; 1500],
509 mem_recorder: None,
510 offset: 2,
511 crc32: 0,
512 is_delta_in_pack: false,
513 known_hash: None,
514 };
515 cache_sha256.insert(obj.offset, hash, obj.clone());
516 assert!(cache_sha256.hash_set.contains(&hash));
517 assert!(cache_sha256.try_get(hash).is_some());
518 });
519
520 handle_sha1.join().unwrap();
521 handle_sha256.join().unwrap();
522
523 assert_eq!(cache.total_inserted(), 2);
524 }
525
526 #[test]
527 fn test_remove_tmp_dir_does_not_panic_when_cleanup_fails() {
528 let dir = tempfile::tempdir().unwrap();
529 let tmp_path = dir.path().join("not-a-directory");
530 fs::write(&tmp_path, b"cache marker").unwrap();
531 let cache = Caches::new(None, tmp_path, 1);
532
533 let result =
534 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| cache.remove_tmp_dir()));
535
536 assert!(result.is_ok(), "cache cleanup should not panic");
537 assert!(
538 result.unwrap().is_err(),
539 "cleanup failure should be returned"
540 );
541 }
542
543 #[test]
544 fn test_unbounded_cache_skips_hash_set_index() {
545 let _guard = set_hash_kind_for_test(HashKind::Sha1);
546 let base = PathBuf::from(env::current_dir().unwrap().parent().unwrap());
547 let tmp_path = base.join("tests/.cache_tmp_unbounded");
548 let cache = Caches::new(None, tmp_path, 1);
549 let hash = ObjectHash::new(b"unbounded-entry");
550 let obj = make_obj(64, hash);
551
552 cache.insert(obj.offset, hash, obj);
553
554 assert!(cache.hash_set.is_empty());
555 assert!(cache.resident_hash_set.contains(&hash));
556 assert_eq!(cache.total_inserted(), 1);
557 assert_eq!(cache.get_hash(0), Some(hash));
558 assert!(cache.try_get(hash).is_some());
559 assert!(cache.get_by_hash(hash).is_some());
560 assert!(
561 cache
562 .get_by_hash(ObjectHash::new(b"missing-entry"))
563 .is_none()
564 );
565 assert!(cache.get_by_offset(0).is_some());
566 assert!(cache.memory_used_index() > 0);
567 }
568
569 #[test]
570 fn test_bounded_cache_tracks_resident_entries() {
571 let _guard = set_hash_kind_for_test(HashKind::Sha1);
572 let base = PathBuf::from(env::current_dir().unwrap().parent().unwrap());
573 let tmp_path = base.join("tests/.cache_tmp_resident");
574 if tmp_path.exists() {
575 fs::remove_dir_all(&tmp_path).unwrap();
576 }
577
578 let cache = Caches::new(Some(2048), tmp_path, 1);
579 let a_hash = ObjectHash::new(b"resident-a");
580 let b_hash = ObjectHash::new(b"resident-b");
581 let a = make_obj(1800, a_hash);
582 let b = make_obj(1800, b_hash);
583
584 cache.insert(a.offset, a_hash, a);
585 assert!(cache.hash_set.contains(&a_hash));
586 assert!(cache.resident_hash_set.contains(&a_hash));
587
588 cache.insert(b.offset + 1, b_hash, b);
589 assert!(cache.hash_set.contains(&a_hash));
590 assert!(cache.hash_set.contains(&b_hash));
591 assert!(!cache.resident_hash_set.contains(&a_hash));
592 assert!(cache.resident_hash_set.contains(&b_hash));
593 }
594}