git_internal/internal/pack/
cache.rs1use 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;
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>, lru_cache: Mutex<LruCache<ObjectHash, ArcWrapper<CacheObject>>>,
62 mem_size: Option<usize>,
63 tmp_path: PathBuf,
64 path_prefixes: [Once; 256],
65 pool: Arc<ThreadPool>,
66 complete_signal: Arc<AtomicBool>,
67}
68
69impl Caches {
70 fn try_get(&self, hash: ObjectHash) -> Option<Arc<CacheObject>> {
72 let mut map = self.lru_cache.lock().unwrap();
73 map.get(&hash).map(|x| x.data.clone())
74 }
75
76 fn get_fallback(&self, hash: ObjectHash) -> io::Result<Arc<CacheObject>> {
79 let path = self.generate_temp_path(&self.tmp_path, hash);
80 let obj = {
82 loop {
83 match Self::read_from_temp(&path) {
84 Ok(x) => break x,
85 Err(e) if e.kind() == io::ErrorKind::NotFound => {
86 sleep(std::time::Duration::from_millis(10));
87 continue;
88 }
89 Err(e) => return Err(e), }
91 }
92 };
93
94 let mut map = self.lru_cache.lock().unwrap();
95 let obj = Arc::new(obj);
96 let mut x = ArcWrapper::new(
97 obj.clone(),
98 self.complete_signal.clone(),
99 Some(self.pool.clone()),
100 );
101 x.set_store_path(path);
102 let _ = map.insert(hash, x); Ok(obj)
104 }
105
106 fn generate_temp_path(&self, tmp_path: &Path, hash: ObjectHash) -> PathBuf {
108 let mut path =
110 PathBuf::with_capacity(self.tmp_path.capacity() + hash.to_string().len() + 5);
111 path.push(tmp_path);
112 path.push(CACHE_LAYOUT_VERSION);
113 let hash_str = hash._to_string();
114 path.push(&hash_str[..2]); self.path_prefixes[hash.as_ref()[0] as usize].call_once(|| {
116 if !path.exists() {
118 fs::create_dir_all(&path).unwrap();
119 }
120 });
121 path.push(hash_str);
122 path
123 }
124
125 fn read_from_temp(path: &Path) -> io::Result<CacheObject> {
127 let obj = CacheObject::f_load(path)?;
128 obj.record_mem_size();
131 Ok(obj)
132 }
133
134 pub fn queued_tasks(&self) -> usize {
136 self.pool.queued_count()
137 }
138
139 pub fn memory_used_index(&self) -> usize {
141 self.map_offset.capacity()
142 * (std::mem::size_of::<usize>() + std::mem::size_of::<ObjectHash>())
143 + self.hash_set.capacity() * (std::mem::size_of::<ObjectHash>())
144 }
145
146 pub(crate) fn shutdown(&self) {
147 time_it!("Caches clear", {
148 self.complete_signal.store(true, Ordering::Release);
149 self.pool.join();
150 self.lru_cache
151 .lock()
152 .unwrap_or_else(|e| e.into_inner())
153 .clear();
154 self.hash_set.clear();
155 self.hash_set.shrink_to_fit();
156 self.map_offset.clear();
157 self.map_offset.shrink_to_fit();
158 });
159 }
160
161 pub fn remove_tmp_dir(&self) -> io::Result<()> {
163 time_it!("Remove tmp dir", {
164 if self.tmp_path.exists() {
165 match fs::remove_dir_all(&self.tmp_path) {
166 Ok(()) => {}
167 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
168 Err(e) => return Err(e),
169 }
170
171 if let Some(parent) = self.tmp_path.parent() {
172 let is_cache_temp = parent
173 .file_name()
174 .and_then(|n| n.to_str())
175 .map(|n| n == ".cache_temp")
176 .unwrap_or(false);
177 if is_cache_temp {
178 match fs::remove_dir(parent) {
179 Ok(()) => {}
180 Err(e)
181 if matches!(
182 e.kind(),
183 io::ErrorKind::DirectoryNotEmpty | io::ErrorKind::NotFound
184 ) => {}
185 Err(e) => return Err(e),
186 }
187 }
188 }
189 }
190
191 Ok(())
192 })
193 }
194}
195
196impl _Cache for Caches {
197 fn new(mem_size: Option<usize>, tmp_path: PathBuf, thread_num: usize) -> Self
200 where
201 Self: Sized,
202 {
203 if mem_size.is_some() {
205 fs::create_dir_all(&tmp_path).unwrap();
206 }
207
208 Caches {
209 map_offset: DashMap::new(),
210 hash_set: DashSet::new(),
211 lru_cache: Mutex::new(LruCache::new(mem_size.unwrap_or(usize::MAX))),
212 mem_size,
213 tmp_path,
214 path_prefixes: [const { Once::new() }; 256],
215 pool: Arc::new(ThreadPool::new(thread_num)),
216 complete_signal: Arc::new(AtomicBool::new(false)),
217 }
218 }
219
220 fn get_hash(&self, offset: usize) -> Option<ObjectHash> {
221 self.map_offset.get(&offset).map(|x| *x)
222 }
223
224 fn insert(&self, offset: usize, hash: ObjectHash, obj: CacheObject) -> Arc<CacheObject> {
225 let obj_arc = Arc::new(obj);
226 {
227 let mut map = self.lru_cache.lock().unwrap();
229 let mut a_obj = ArcWrapper::new(
230 obj_arc.clone(),
231 self.complete_signal.clone(),
232 Some(self.pool.clone()),
233 );
234 if self.mem_size.is_some() {
235 a_obj.set_store_path(self.generate_temp_path(&self.tmp_path, hash));
236 }
237 let _ = map.insert(hash, a_obj);
238 }
239 self.hash_set.insert(hash);
241 self.map_offset.insert(offset, hash);
242
243 obj_arc
244 }
245
246 fn get_by_offset(&self, offset: usize) -> Option<Arc<CacheObject>> {
248 match self.map_offset.get(&offset) {
249 Some(x) => self.get_by_hash(*x),
250 None => None,
251 }
252 }
253
254 fn get_by_hash(&self, hash: ObjectHash) -> Option<Arc<CacheObject>> {
256 if self.hash_set.contains(&hash) {
258 match self.try_get(hash) {
259 Some(x) => Some(x),
260 None => {
261 if self.mem_size.is_none() {
262 panic!("should not be here when mem_size is not set")
263 }
264 self.get_fallback(hash).ok()
265 }
266 }
267 } else {
268 None
269 }
270 }
271
272 fn total_inserted(&self) -> usize {
273 self.hash_set.len()
274 }
275 fn memory_used(&self) -> usize {
276 self.lru_cache.lock().unwrap().current_size() + self.memory_used_index()
277 }
278 fn clear(&self) {
279 self.shutdown();
280
281 assert_eq!(self.pool.queued_count(), 0);
282 assert_eq!(self.pool.active_count(), 0);
283 assert_eq!(self.lru_cache.lock().unwrap().len(), 0);
284 }
285}
286
287#[cfg(test)]
288mod test {
289 use std::{env, sync::Arc, thread};
290
291 use super::*;
292 use crate::{
293 hash::{HashKind, ObjectHash, set_hash_kind_for_test},
294 internal::{object::types::ObjectType, pack::cache_object::CacheObjectInfo},
295 };
296
297 fn make_obj(size: usize, hash: ObjectHash) -> CacheObject {
299 CacheObject {
300 info: CacheObjectInfo::BaseObject(ObjectType::Blob, hash),
301 data_decompressed: vec![0; size],
302 mem_recorder: None,
303 offset: 0,
304 crc32: 0,
305 is_delta_in_pack: false,
306 }
307 }
308
309 #[test]
311 fn test_cache_single_thread() {
312 for (kind, cap, size_ab, size_c, tmp_dir) in [
313 (
314 HashKind::Sha1,
315 2048usize,
316 800usize,
317 1700usize,
318 "tests/.cache_tmp",
319 ),
320 (
321 HashKind::Sha256,
322 4096usize,
323 1500usize,
324 3000usize,
325 "tests/.cache_tmp_sha256",
326 ),
327 ] {
328 let _guard = set_hash_kind_for_test(kind);
329 let source = PathBuf::from(env::current_dir().unwrap().parent().unwrap());
330 let tmp_path = source.clone().join(tmp_dir);
331 if tmp_path.exists() {
332 fs::remove_dir_all(&tmp_path).unwrap();
333 }
334
335 let cache = Caches::new(Some(cap), tmp_path, 1);
336 let a_hash = ObjectHash::new(String::from("a").as_bytes());
337 let b_hash = ObjectHash::new(String::from("b").as_bytes());
338 let c_hash = ObjectHash::new(String::from("c").as_bytes());
339
340 let a = make_obj(size_ab, a_hash);
341 let b = make_obj(size_ab, b_hash);
342 let c = make_obj(size_c, c_hash);
343
344 cache.insert(a.offset, a_hash, a.clone());
346 assert!(cache.hash_set.contains(&a_hash));
347 assert!(cache.try_get(a_hash).is_some());
348
349 cache.insert(b.offset, b_hash, b.clone());
351 assert!(cache.hash_set.contains(&b_hash));
352 assert!(cache.try_get(b_hash).is_some());
353 assert!(cache.try_get(a_hash).is_some());
354
355 cache.insert(c.offset, c_hash, c.clone());
357 assert!(cache.try_get(a_hash).is_none());
358 assert!(cache.try_get(b_hash).is_none());
359 assert!(cache.try_get(c_hash).is_some());
360 assert!(cache.get_by_hash(c_hash).is_some());
361 }
362 }
363
364 #[test]
366 fn test_cache_multi_thread_mixed_hash_kinds() {
367 let base = PathBuf::from(env::current_dir().unwrap().parent().unwrap());
368 let tmp_path = base.join("tests/.cache_tmp_mixed");
369 if tmp_path.exists() {
370 fs::remove_dir_all(&tmp_path).unwrap();
371 }
372
373 let cache = Arc::new(Caches::new(Some(4096), tmp_path, 2));
374
375 let cache_sha1 = Arc::clone(&cache);
376 let handle_sha1 = thread::spawn(move || {
377 let _g = set_hash_kind_for_test(HashKind::Sha1);
378 let hash = ObjectHash::new(b"sha1-entry");
379 let obj = CacheObject {
380 info: CacheObjectInfo::BaseObject(ObjectType::Blob, hash),
381 data_decompressed: vec![0; 800],
382 mem_recorder: None,
383 offset: 1,
384 crc32: 0,
385 is_delta_in_pack: false,
386 };
387 cache_sha1.insert(obj.offset, hash, obj.clone());
388 assert!(cache_sha1.hash_set.contains(&hash));
389 assert!(cache_sha1.try_get(hash).is_some());
390 });
391
392 let cache_sha256 = Arc::clone(&cache);
393 let handle_sha256 = thread::spawn(move || {
394 let _g = set_hash_kind_for_test(HashKind::Sha256);
395 let hash = ObjectHash::new(b"sha256-entry");
396 let obj = CacheObject {
397 info: CacheObjectInfo::BaseObject(ObjectType::Blob, hash),
398 data_decompressed: vec![0; 1500],
399 mem_recorder: None,
400 offset: 2,
401 crc32: 0,
402 is_delta_in_pack: false,
403 };
404 cache_sha256.insert(obj.offset, hash, obj.clone());
405 assert!(cache_sha256.hash_set.contains(&hash));
406 assert!(cache_sha256.try_get(hash).is_some());
407 });
408
409 handle_sha1.join().unwrap();
410 handle_sha256.join().unwrap();
411
412 assert_eq!(cache.total_inserted(), 2);
413 }
414
415 #[test]
416 fn test_remove_tmp_dir_does_not_panic_when_cleanup_fails() {
417 let dir = tempfile::tempdir().unwrap();
418 let tmp_path = dir.path().join("not-a-directory");
419 fs::write(&tmp_path, b"cache marker").unwrap();
420 let cache = Caches::new(None, tmp_path, 1);
421
422 let result =
423 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| cache.remove_tmp_dir()));
424
425 assert!(result.is_ok(), "cache cleanup should not panic");
426 assert!(
427 result.unwrap().is_err(),
428 "cleanup failure should be returned"
429 );
430 }
431}