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