Skip to main content

grit_lib/
pack_store.rs

1//! Repository-scoped pack and MIDX read caches.
2//!
3//! [`PackStore`] holds parsed pack indexes, pack file bytes, directory listings (with mtime
4//! signatures for reprepare-on-miss), multi-pack-index views, and the delta-base LRU. Each
5//! `objects/` directory gets its own store; [`crate::odb::Odb`] owns an [`Arc<PackStore>`]
6//! and installs it as thread-local context during [`Odb::read`](crate::odb::Odb::read).
7//! After repack or index-pack, call [`Odb::invalidate_packs`](crate::odb::Odb::invalidate_packs)
8//! on any handle for that repository so cached listings do not outlive removed pack files.
9
10use std::cell::RefCell;
11use std::collections::HashMap;
12use std::path::{Path, PathBuf};
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::{Arc, Mutex, RwLock};
15
16/// Counters and hooks for pack-store integration tests.
17pub mod test_hooks {
18    use std::sync::atomic::{AtomicUsize, Ordering};
19
20    static LEGACY_STORES_CREATED: AtomicUsize = AtomicUsize::new(0);
21    static IMPLICIT_STORES_CREATED: AtomicUsize = AtomicUsize::new(0);
22
23    /// `reset` function.
24    pub fn reset() {
25        LEGACY_STORES_CREATED.store(0, Ordering::Release);
26        IMPLICIT_STORES_CREATED.store(0, Ordering::Release);
27    }
28
29    /// `legacy_stores_created` function.
30    pub fn legacy_stores_created() -> usize {
31        LEGACY_STORES_CREATED.load(Ordering::Acquire)
32    }
33
34    /// `implicit_stores_created` function.
35    pub fn implicit_stores_created() -> usize {
36        IMPLICIT_STORES_CREATED.load(Ordering::Acquire)
37    }
38
39    pub(crate) fn record_legacy_store_created() {
40        LEGACY_STORES_CREATED.fetch_add(1, Ordering::Relaxed);
41    }
42
43    pub(crate) fn record_implicit_store_created() {
44        IMPLICIT_STORES_CREATED.fetch_add(1, Ordering::Relaxed);
45    }
46}
47
48/// Per-`objects/` cache of pack indexes, bytes, MIDX views, and delta-base LRU state.
49///
50/// [`crate::odb::Odb`] owns an [`Arc<PackStore>`]; [`Odb`](crate::odb::Odb) clones share it.
51/// Independently opened [`Odb`](crate::odb::Odb) handles for the same path get separate stores.
52/// Alternate object directories get separate stores retained on the parent [`Odb`](crate::odb::Odb).
53pub struct PackStore {
54    objects_dir: PathBuf,
55    pub(crate) sparse_read_files: crate::pack_map::SparseReadFiles,
56    bounded_reads: bool,
57    /// Bumped on every invalidation so other threads drop trusted thread-local pack caches.
58    cache_epoch: AtomicU64,
59    catalog: RwLock<crate::pack::pack_cache::CatalogState>,
60    delta_bases: crate::pack::pack_cache::ShardedDeltaBaseLru,
61    midx: Mutex<crate::midx::midx_cache::State>,
62    /// Repository-scoped config cache for MIDX chain parsing (paired with [`crate::odb::Odb`]).
63    repo_caches: RwLock<Option<Arc<crate::repo_caches::RepoCaches>>>,
64}
65
66/// Tree delta working set for an operation that reads scattered historical entries.
67pub(crate) const BOUNDED_DELTA_BASE_BYTES: usize = 16 * 1024 * 1024;
68
69const _: () = {
70    fn assert_send_sync<T: Send + Sync>() {}
71    let _ = assert_send_sync::<PackStore>;
72};
73
74// hygiene: scoped read contexts and legacy standalone caches must not cross threads
75thread_local! {
76    static CURRENT: RefCell<Option<Arc<PackStore>>> = const { RefCell::new(None) };
77    static ACTIVE_OBJECTS_DIR: RefCell<Option<PathBuf>> = const { RefCell::new(None) };
78    /// Standalone pack reads (no [`crate::odb::Odb`]) reuse one store per `objects/` on this thread.
79    static LEGACY_BY_DIR: RefCell<HashMap<PathBuf, Arc<PackStore>>> = RefCell::new(HashMap::new());
80    static IMPLICIT: RefCell<Option<Arc<PackStore>>> = const { RefCell::new(None) };
81}
82
83impl PackStore {
84    /// Create a fresh store for `objects_dir` (not shared with other independent [`Odb`](crate::odb::Odb) opens).
85    #[must_use]
86    pub fn new(objects_dir: PathBuf) -> Self {
87        Self {
88            objects_dir,
89            sparse_read_files: crate::pack_map::SparseReadFiles::default(),
90            bounded_reads: false,
91            cache_epoch: AtomicU64::new(0),
92            catalog: RwLock::new(crate::pack::pack_cache::CatalogState::default()),
93            delta_bases: crate::pack::pack_cache::ShardedDeltaBaseLru::new(),
94            midx: Mutex::new(crate::midx::midx_cache::State::default()),
95            repo_caches: RwLock::new(None),
96        }
97    }
98
99    /// Private cache for a random-access history operation. The pack catalog retains
100    /// at most 32 readers (descriptor and compressed windows together); active reads
101    /// retain their own handles until completion. Never changes a shared store.
102    pub(crate) fn for_bounded_reads(objects_dir: PathBuf) -> Self {
103        let mut store = Self::new(objects_dir);
104        store.bounded_reads = true;
105        store.delta_bases.set_byte_limit(BOUNDED_DELTA_BASE_BYTES);
106        store
107    }
108
109    pub(crate) fn bounded_reads(&self) -> bool {
110        self.bounded_reads
111    }
112
113    /// Share the open repository's config cache for pack/MIDX reads on this store.
114    pub(crate) fn set_repo_caches(&self, caches: Arc<crate::repo_caches::RepoCaches>) {
115        if let Ok(mut guard) = self.repo_caches.write() {
116            *guard = Some(caches);
117        }
118    }
119
120    fn repo_caches(&self) -> Option<Arc<crate::repo_caches::RepoCaches>> {
121        self.repo_caches
122            .read()
123            .ok()
124            .and_then(|g| g.as_ref().map(Arc::clone))
125    }
126
127    /// Config cache for MIDX reads against `objects_dir`, when opened via [`crate::odb::Odb`].
128    pub(crate) fn repo_caches_for_objects_dir(
129        objects_dir: &Path,
130    ) -> Option<Arc<crate::repo_caches::RepoCaches>> {
131        if Self::has_context() {
132            let store = Self::active_for_reads();
133            if store.objects_dir() == objects_dir {
134                return store.repo_caches();
135            }
136        }
137        LEGACY_BY_DIR.with(|map| {
138            map.borrow()
139                .get(objects_dir)
140                .and_then(|store| store.repo_caches())
141        })
142    }
143
144    /// Like [`Self::repo_caches_for_objects_dir`] using a repository `config` path.
145    pub(crate) fn repo_caches_for_config_path(
146        config_path: &Path,
147    ) -> Option<Arc<crate::repo_caches::RepoCaches>> {
148        let git_dir = config_path.parent()?;
149        Self::repo_caches_for_objects_dir(&git_dir.join("objects"))
150    }
151
152    /// Generation counter incremented when pack caches are invalidated.
153    #[must_use]
154    pub(crate) fn cache_epoch(&self) -> u64 {
155        self.cache_epoch.load(Ordering::Acquire)
156    }
157
158    pub(crate) fn bump_cache_epoch(&self) -> u64 {
159        self.cache_epoch.fetch_add(1, Ordering::Release) + 1
160    }
161
162    /// Pack catalog (indexes, bytes, directory listings) for this store.
163    pub(crate) fn with_catalog<R>(
164        &self,
165        f: impl FnOnce(&mut crate::pack::pack_cache::CatalogState) -> R,
166    ) -> R {
167        f(&mut self.catalog.write().unwrap_or_else(|e| e.into_inner()))
168    }
169
170    pub(crate) fn with_catalog_read<R>(
171        &self,
172        f: impl FnOnce(&crate::pack::pack_cache::CatalogState) -> R,
173    ) -> R {
174        f(&self.catalog.read().unwrap_or_else(|e| e.into_inner()))
175    }
176
177    /// Like [`Self::with_catalog`], but skips `f` when the catalog write lock is contended.
178    pub(crate) fn try_with_catalog<R>(
179        &self,
180        f: impl FnOnce(&mut crate::pack::pack_cache::CatalogState) -> R,
181    ) -> Option<R> {
182        let mut guard = self.catalog.try_write().ok()?;
183        Some(f(&mut guard))
184    }
185
186    /// Sharded delta-base LRU (independent of the catalog mutex).
187    #[must_use]
188    pub(crate) fn delta_bases(&self) -> &crate::pack::pack_cache::ShardedDeltaBaseLru {
189        &self.delta_bases
190    }
191
192    /// Resolve the [`PackStore`] used for pack-cache reads on this thread.
193    pub(crate) fn active_for_reads() -> Arc<Self> {
194        if let Some(store) = CURRENT.with(|c| c.borrow().clone()) {
195            return store;
196        }
197        if let Some(dir) = ACTIVE_OBJECTS_DIR.with(|d| d.borrow().clone()) {
198            return Self::legacy_for_objects_dir(&dir);
199        }
200        IMPLICIT.with(|i| {
201            if let Some(s) = i.borrow().as_ref() {
202                return Arc::clone(s);
203            }
204            test_hooks::record_implicit_store_created();
205            let s = Arc::new(Self::new(PathBuf::from("__grit_implicit_pack_store")));
206            *i.borrow_mut() = Some(Arc::clone(&s));
207            s
208        })
209    }
210
211    /// Legacy standalone reads: thread-local store for `objects_dir` (not shared with [`Odb`]).
212    pub(crate) fn legacy_for_objects_dir(objects_dir: &Path) -> Arc<Self> {
213        LEGACY_BY_DIR.with(|map| {
214            let key = objects_dir.to_path_buf();
215            if let Some(s) = map.borrow().get(&key) {
216                return Arc::clone(s);
217            }
218            test_hooks::record_legacy_store_created();
219            let store = Arc::new(Self::new(key.clone()));
220            map.borrow_mut().insert(key, Arc::clone(&store));
221            store
222        })
223    }
224
225    /// Store for pack reads under a standard `…/objects/pack/*.pack` path (legacy API only).
226    pub(crate) fn legacy_for_pack_path(pack_path: &Path) -> Arc<Self> {
227        let objects_dir = pack_path.parent().and_then(|pack_dir| {
228            (pack_dir.file_name() == Some(std::ffi::OsStr::new("pack")))
229                .then(|| pack_dir.parent())
230                .flatten()
231        });
232        match objects_dir {
233            Some(dir) => Self::legacy_for_objects_dir(dir),
234            None => IMPLICIT.with(|i| {
235                if let Some(s) = i.borrow().as_ref() {
236                    return Arc::clone(s);
237                }
238                test_hooks::record_implicit_store_created();
239                let s = Arc::new(Self::new(PathBuf::from("__grit_nonstandard_pack_cache")));
240                *i.borrow_mut() = Some(Arc::clone(&s));
241                s
242            }),
243        }
244    }
245
246    /// `objects/` path for this store.
247    #[must_use]
248    pub fn objects_dir(&self) -> &Path {
249        &self.objects_dir
250    }
251
252    /// Whether this thread already has an active pack-read store context.
253    #[must_use]
254    pub fn has_context() -> bool {
255        CURRENT.with(|c| c.borrow().is_some())
256    }
257
258    /// Whether the active read context is scoped to `objects_dir`.
259    #[must_use]
260    pub(crate) fn read_context_matches_objects_dir(objects_dir: &Path) -> bool {
261        Self::has_context()
262            && ACTIVE_OBJECTS_DIR.with(|d| d.borrow().as_deref() == Some(objects_dir))
263    }
264
265    /// Run `f` with `store` as the pack-read context on this thread.
266    ///
267    /// Nested calls for the same store reuse the current context. Distinct store
268    /// handles remain isolated even when they name the same object directory.
269    pub fn with_context<R>(store: Arc<Self>, f: impl FnOnce() -> R) -> R {
270        if CURRENT.with(|current| {
271            current
272                .borrow()
273                .as_ref()
274                .is_some_and(|active| Arc::ptr_eq(active, &store))
275        }) {
276            return f();
277        }
278        let _guard = PackReadContextGuard::install(store);
279        f()
280    }
281
282    /// Drop cached pack listings, indexes, pack bytes, and delta bases.
283    pub fn invalidate_packs(&self) {
284        crate::pack::pack_cache::clear_catalog_on(&self.catalog);
285        self.delta_bases.clear();
286        self.sparse_read_files.clear();
287        let epoch = self.bump_cache_epoch();
288        crate::pack::pack_cache::clear_thread_local_after_invalidation(epoch);
289    }
290
291    /// Drop MIDX bytes and tip cache under this store's `objects/pack/`.
292    pub fn invalidate_midx(&self) {
293        crate::midx::midx_cache::evict_pack_dir_on(&self.midx, &self.objects_dir.join("pack"));
294    }
295
296    /// Full pack + MIDX invalidation (after repack/gc/index-pack in-process).
297    pub fn invalidate_all(&self) {
298        self.invalidate_packs();
299        self.invalidate_midx();
300    }
301
302    /// Apply `core.deltaBaseCacheLimit` from config to this store's delta-base LRU.
303    pub fn configure_delta_base_from_config(&self, cfg: Option<&crate::config::ConfigSet>) {
304        crate::pack::pack_cache::configure_delta_base_on(&self.delta_bases, cfg);
305    }
306
307    pub(crate) fn with_pack<R>(
308        &self,
309        f: impl FnOnce(&mut crate::pack::pack_cache::CatalogState) -> R,
310    ) -> R {
311        self.with_catalog(f)
312    }
313
314    pub(crate) fn with_midx<R>(
315        &self,
316        f: impl FnOnce(&mut crate::midx::midx_cache::State) -> R,
317    ) -> R {
318        f(&mut self.midx.lock().unwrap_or_else(|e| e.into_inner()))
319    }
320
321    /// Run `f` on the store for the active read context (current store or legacy fallback).
322    pub(crate) fn with_current_or_implicit<R>(
323        f: impl FnOnce(&mut crate::pack::pack_cache::CatalogState) -> R,
324    ) -> R {
325        Self::active_for_reads().with_pack(f)
326    }
327
328    pub(crate) fn with_current_or_implicit_midx<R>(
329        f: impl FnOnce(&mut crate::midx::midx_cache::State) -> R,
330    ) -> R {
331        if let Some(store) = CURRENT.with(|c| c.borrow().clone()) {
332            return store.with_midx(f);
333        }
334        if let Some(dir) = ACTIVE_OBJECTS_DIR.with(|d| d.borrow().clone()) {
335            return Self::legacy_for_objects_dir(&dir).with_midx(f);
336        }
337        Self::active_for_reads().with_midx(f)
338    }
339
340    /// Clear legacy thread-local stores (tests/benches calling [`crate::pack::clear_pack_cache`]).
341    pub fn invalidate_all_legacy() {
342        LEGACY_BY_DIR.with(|map| {
343            for store in map.borrow().values() {
344                store.invalidate_all();
345            }
346            map.borrow_mut().clear();
347        });
348        IMPLICIT.with(|i| *i.borrow_mut() = None);
349    }
350
351    /// Drop cached pack state for legacy standalone reads of `objects_dir`.
352    pub(crate) fn invalidate_legacy_for_objects_dir(objects_dir: &Path) {
353        LEGACY_BY_DIR.with(|map| {
354            if let Some(store) = map.borrow().get(objects_dir) {
355                store.invalidate_all();
356            }
357        });
358    }
359}
360
361/// Keeps a thread-local pack read context alive for batch parallel reads.
362pub(crate) struct PackReadContextGuard {
363    prev_store: Option<Arc<PackStore>>,
364    prev_dir: Option<PathBuf>,
365}
366
367impl PackReadContextGuard {
368    pub(crate) fn install(store: Arc<PackStore>) -> Self {
369        let prev_store = CURRENT.with(|c| c.borrow_mut().replace(Arc::clone(&store)));
370        let prev_dir =
371            ACTIVE_OBJECTS_DIR.with(|d| d.borrow_mut().replace(store.objects_dir().to_path_buf()));
372        crate::pack::pack_cache::warm_thread_local_from_store(&store);
373        Self {
374            prev_store,
375            prev_dir,
376        }
377    }
378}
379
380impl Drop for PackReadContextGuard {
381    fn drop(&mut self) {
382        if CURRENT.with(|c| c.borrow().as_ref().is_some_and(|s| s.bounded_reads())) {
383            // Operation-local pack bytes/indexes must not outlive their cache through
384            // thread-local fast paths, including when the allocator reuses a store address.
385            crate::pack::pack_cache::clear_thread_local_after_invalidation(0);
386        }
387        crate::pack::clear_pack_read_info_cache();
388        ACTIVE_OBJECTS_DIR.with(|d| *d.borrow_mut() = self.prev_dir.take());
389        CURRENT.with(|c| *c.borrow_mut() = self.prev_store.take());
390    }
391}