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