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    /// Bumped on every invalidation so other threads drop trusted thread-local pack caches.
56    cache_epoch: AtomicU64,
57    catalog: RwLock<crate::pack::pack_cache::CatalogState>,
58    delta_bases: crate::pack::pack_cache::ShardedDeltaBaseLru,
59    midx: Mutex<crate::midx::midx_cache::State>,
60    /// Repository-scoped config cache for MIDX chain parsing (paired with [`crate::odb::Odb`]).
61    repo_caches: RwLock<Option<Arc<crate::repo_caches::RepoCaches>>>,
62}
63
64const _: () = {
65    fn assert_send_sync<T: Send + Sync>() {}
66    let _ = assert_send_sync::<PackStore>;
67};
68
69// hygiene: scoped read contexts and legacy standalone caches must not cross threads
70thread_local! {
71    static CURRENT: RefCell<Option<Arc<PackStore>>> = const { RefCell::new(None) };
72    static ACTIVE_OBJECTS_DIR: RefCell<Option<PathBuf>> = const { RefCell::new(None) };
73    /// Standalone pack reads (no [`crate::odb::Odb`]) reuse one store per `objects/` on this thread.
74    static LEGACY_BY_DIR: RefCell<HashMap<PathBuf, Arc<PackStore>>> = RefCell::new(HashMap::new());
75    static IMPLICIT: RefCell<Option<Arc<PackStore>>> = const { RefCell::new(None) };
76}
77
78impl PackStore {
79    /// Create a fresh store for `objects_dir` (not shared with other independent [`Odb`](crate::odb::Odb) opens).
80    #[must_use]
81    pub fn new(objects_dir: PathBuf) -> Self {
82        Self {
83            objects_dir,
84            cache_epoch: AtomicU64::new(0),
85            catalog: RwLock::new(crate::pack::pack_cache::CatalogState::default()),
86            delta_bases: crate::pack::pack_cache::ShardedDeltaBaseLru::new(),
87            midx: Mutex::new(crate::midx::midx_cache::State::default()),
88            repo_caches: RwLock::new(None),
89        }
90    }
91
92    /// Share the open repository's config cache for pack/MIDX reads on this store.
93    pub(crate) fn set_repo_caches(&self, caches: Arc<crate::repo_caches::RepoCaches>) {
94        if let Ok(mut guard) = self.repo_caches.write() {
95            *guard = Some(caches);
96        }
97    }
98
99    fn repo_caches(&self) -> Option<Arc<crate::repo_caches::RepoCaches>> {
100        self.repo_caches
101            .read()
102            .ok()
103            .and_then(|g| g.as_ref().map(Arc::clone))
104    }
105
106    /// Config cache for MIDX reads against `objects_dir`, when opened via [`crate::odb::Odb`].
107    pub(crate) fn repo_caches_for_objects_dir(
108        objects_dir: &Path,
109    ) -> Option<Arc<crate::repo_caches::RepoCaches>> {
110        if Self::has_context() {
111            let store = Self::active_for_reads();
112            if store.objects_dir() == objects_dir {
113                return store.repo_caches();
114            }
115        }
116        LEGACY_BY_DIR.with(|map| {
117            map.borrow()
118                .get(objects_dir)
119                .and_then(|store| store.repo_caches())
120        })
121    }
122
123    /// Like [`Self::repo_caches_for_objects_dir`] using a repository `config` path.
124    pub(crate) fn repo_caches_for_config_path(
125        config_path: &Path,
126    ) -> Option<Arc<crate::repo_caches::RepoCaches>> {
127        let git_dir = config_path.parent()?;
128        Self::repo_caches_for_objects_dir(&git_dir.join("objects"))
129    }
130
131    /// Generation counter incremented when pack caches are invalidated.
132    #[must_use]
133    pub(crate) fn cache_epoch(&self) -> u64 {
134        self.cache_epoch.load(Ordering::Acquire)
135    }
136
137    pub(crate) fn bump_cache_epoch(&self) -> u64 {
138        self.cache_epoch.fetch_add(1, Ordering::Release) + 1
139    }
140
141    /// Pack catalog (indexes, bytes, directory listings) for this store.
142    pub(crate) fn with_catalog<R>(
143        &self,
144        f: impl FnOnce(&mut crate::pack::pack_cache::CatalogState) -> R,
145    ) -> R {
146        f(&mut self.catalog.write().unwrap_or_else(|e| e.into_inner()))
147    }
148
149    pub(crate) fn with_catalog_read<R>(
150        &self,
151        f: impl FnOnce(&crate::pack::pack_cache::CatalogState) -> R,
152    ) -> R {
153        f(&self.catalog.read().unwrap_or_else(|e| e.into_inner()))
154    }
155
156    /// Like [`Self::with_catalog`], but skips `f` when the catalog write lock is contended.
157    pub(crate) fn try_with_catalog<R>(
158        &self,
159        f: impl FnOnce(&mut crate::pack::pack_cache::CatalogState) -> R,
160    ) -> Option<R> {
161        let mut guard = self.catalog.try_write().ok()?;
162        Some(f(&mut guard))
163    }
164
165    /// Sharded delta-base LRU (independent of the catalog mutex).
166    #[must_use]
167    pub(crate) fn delta_bases(&self) -> &crate::pack::pack_cache::ShardedDeltaBaseLru {
168        &self.delta_bases
169    }
170
171    /// Resolve the [`PackStore`] used for pack-cache reads on this thread.
172    pub(crate) fn active_for_reads() -> Arc<Self> {
173        if let Some(store) = CURRENT.with(|c| c.borrow().clone()) {
174            return store;
175        }
176        if let Some(dir) = ACTIVE_OBJECTS_DIR.with(|d| d.borrow().clone()) {
177            return Self::legacy_for_objects_dir(&dir);
178        }
179        IMPLICIT.with(|i| {
180            if let Some(s) = i.borrow().as_ref() {
181                return Arc::clone(s);
182            }
183            test_hooks::record_implicit_store_created();
184            let s = Arc::new(Self::new(PathBuf::from("__grit_implicit_pack_store")));
185            *i.borrow_mut() = Some(Arc::clone(&s));
186            s
187        })
188    }
189
190    /// Legacy standalone reads: thread-local store for `objects_dir` (not shared with [`Odb`]).
191    pub(crate) fn legacy_for_objects_dir(objects_dir: &Path) -> Arc<Self> {
192        LEGACY_BY_DIR.with(|map| {
193            let key = objects_dir.to_path_buf();
194            if let Some(s) = map.borrow().get(&key) {
195                return Arc::clone(s);
196            }
197            test_hooks::record_legacy_store_created();
198            let store = Arc::new(Self::new(key.clone()));
199            map.borrow_mut().insert(key, Arc::clone(&store));
200            store
201        })
202    }
203
204    /// Store for pack reads under a standard `…/objects/pack/*.pack` path (legacy API only).
205    pub(crate) fn legacy_for_pack_path(pack_path: &Path) -> Arc<Self> {
206        let objects_dir = pack_path.parent().and_then(|pack_dir| {
207            (pack_dir.file_name() == Some(std::ffi::OsStr::new("pack")))
208                .then(|| pack_dir.parent())
209                .flatten()
210        });
211        match objects_dir {
212            Some(dir) => Self::legacy_for_objects_dir(dir),
213            None => IMPLICIT.with(|i| {
214                if let Some(s) = i.borrow().as_ref() {
215                    return Arc::clone(s);
216                }
217                test_hooks::record_implicit_store_created();
218                let s = Arc::new(Self::new(PathBuf::from("__grit_nonstandard_pack_cache")));
219                *i.borrow_mut() = Some(Arc::clone(&s));
220                s
221            }),
222        }
223    }
224
225    /// `objects/` path for this store.
226    #[must_use]
227    pub fn objects_dir(&self) -> &Path {
228        &self.objects_dir
229    }
230
231    /// Whether this thread already has an active pack-read store context.
232    #[must_use]
233    pub fn has_context() -> bool {
234        CURRENT.with(|c| c.borrow().is_some())
235    }
236
237    /// Whether the active read context is scoped to `objects_dir`.
238    #[must_use]
239    pub(crate) fn read_context_matches_objects_dir(objects_dir: &Path) -> bool {
240        Self::has_context()
241            && ACTIVE_OBJECTS_DIR.with(|d| d.borrow().as_deref() == Some(objects_dir))
242    }
243
244    /// Run `f` with `store` as the pack-read context on this thread.
245    pub fn with_context<R>(store: Arc<Self>, f: impl FnOnce() -> R) -> R {
246        let _guard = PackReadContextGuard::install(store);
247        f()
248    }
249
250    /// Drop cached pack listings, indexes, pack bytes, and delta bases.
251    pub fn invalidate_packs(&self) {
252        crate::pack::pack_cache::clear_catalog_on(&self.catalog);
253        self.delta_bases.clear();
254        let epoch = self.bump_cache_epoch();
255        crate::pack::pack_cache::clear_thread_local_after_invalidation(epoch);
256    }
257
258    /// Drop MIDX bytes and tip cache under this store's `objects/pack/`.
259    pub fn invalidate_midx(&self) {
260        crate::midx::midx_cache::evict_pack_dir_on(&self.midx, &self.objects_dir.join("pack"));
261    }
262
263    /// Full pack + MIDX invalidation (after repack/gc/index-pack in-process).
264    pub fn invalidate_all(&self) {
265        self.invalidate_packs();
266        self.invalidate_midx();
267    }
268
269    /// Apply `core.deltaBaseCacheLimit` from config to this store's delta-base LRU.
270    pub fn configure_delta_base_from_config(&self, cfg: Option<&crate::config::ConfigSet>) {
271        crate::pack::pack_cache::configure_delta_base_on(&self.delta_bases, cfg);
272    }
273
274    pub(crate) fn with_pack<R>(
275        &self,
276        f: impl FnOnce(&mut crate::pack::pack_cache::CatalogState) -> R,
277    ) -> R {
278        self.with_catalog(f)
279    }
280
281    pub(crate) fn with_midx<R>(
282        &self,
283        f: impl FnOnce(&mut crate::midx::midx_cache::State) -> R,
284    ) -> R {
285        f(&mut self.midx.lock().unwrap_or_else(|e| e.into_inner()))
286    }
287
288    /// Run `f` on the store for the active read context (current store or legacy fallback).
289    pub(crate) fn with_current_or_implicit<R>(
290        f: impl FnOnce(&mut crate::pack::pack_cache::CatalogState) -> R,
291    ) -> R {
292        Self::active_for_reads().with_pack(f)
293    }
294
295    pub(crate) fn with_current_or_implicit_midx<R>(
296        f: impl FnOnce(&mut crate::midx::midx_cache::State) -> R,
297    ) -> R {
298        if let Some(store) = CURRENT.with(|c| c.borrow().clone()) {
299            return store.with_midx(f);
300        }
301        if let Some(dir) = ACTIVE_OBJECTS_DIR.with(|d| d.borrow().clone()) {
302            return Self::legacy_for_objects_dir(&dir).with_midx(f);
303        }
304        Self::active_for_reads().with_midx(f)
305    }
306
307    /// Clear legacy thread-local stores (tests/benches calling [`crate::pack::clear_pack_cache`]).
308    pub fn invalidate_all_legacy() {
309        LEGACY_BY_DIR.with(|map| {
310            for store in map.borrow().values() {
311                store.invalidate_all();
312            }
313            map.borrow_mut().clear();
314        });
315        IMPLICIT.with(|i| *i.borrow_mut() = None);
316    }
317
318    /// Drop cached pack state for legacy standalone reads of `objects_dir`.
319    pub(crate) fn invalidate_legacy_for_objects_dir(objects_dir: &Path) {
320        LEGACY_BY_DIR.with(|map| {
321            if let Some(store) = map.borrow().get(objects_dir) {
322                store.invalidate_all();
323            }
324        });
325    }
326}
327
328/// Keeps a thread-local pack read context alive for batch parallel reads.
329pub(crate) struct PackReadContextGuard {
330    prev_store: Option<Arc<PackStore>>,
331    prev_dir: Option<PathBuf>,
332}
333
334impl PackReadContextGuard {
335    pub(crate) fn install(store: Arc<PackStore>) -> Self {
336        let prev_store = CURRENT.with(|c| c.borrow_mut().replace(Arc::clone(&store)));
337        let prev_dir =
338            ACTIVE_OBJECTS_DIR.with(|d| d.borrow_mut().replace(store.objects_dir().to_path_buf()));
339        crate::pack::pack_cache::warm_thread_local_from_store(&store);
340        Self {
341            prev_store,
342            prev_dir,
343        }
344    }
345}
346
347impl Drop for PackReadContextGuard {
348    fn drop(&mut self) {
349        crate::pack::clear_pack_read_info_cache();
350        ACTIVE_OBJECTS_DIR.with(|d| *d.borrow_mut() = self.prev_dir.take());
351        CURRENT.with(|c| *c.borrow_mut() = self.prev_store.take());
352    }
353}