Skip to main content

grit_lib/odb/store/
midx.rs

1//! Multi-pack-index object backend ([`MidxObjects`]).
2//!
3//! OID membership and prefix search use the prepared MIDX chain; object bytes are read at the
4//! layer-resolved pack offset via [`PreparedMidxChain::try_read_object`].
5
6use std::collections::HashSet;
7use std::io::Cursor;
8use std::ops::ControlFlow;
9use std::sync::{Arc, RwLock};
10
11use crate::diagnostics::NullDiagnostics;
12use crate::error::{Error, Result};
13use crate::midx::{prepared_midx_chain_strict, PreparedMidxChain};
14use crate::objects::{HashAlgo, Object, ObjectId, ObjectIdPrefix, ObjectInfo};
15use crate::pack::clear_pack_cache;
16use crate::pack_store::PackStore;
17
18use super::{ObjectStore, ObjectStream, PackedObjects};
19
20/// Whether [`MidxObjects`] can serve reads for this repository.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum MidxObjectsStatus {
23    /// `core.multiPackIndex` was off at construction; this backend is inert.
24    Disabled,
25    /// A usable MIDX chain is loaded.
26    Active,
27    /// MIDX was requested but no layer could be loaded (missing or corrupt); fall back to packs.
28    Unusable,
29}
30
31/// MIDX-backed read-only [`ObjectStore`] for one `objects/` directory.
32pub struct MidxObjects {
33    packs: PackedObjects,
34    multi_pack_index: bool,
35    status: RwLock<MidxObjectsStatus>,
36}
37
38impl std::fmt::Debug for MidxObjects {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.debug_struct("MidxObjects")
41            .field("objects_dir", &self.packs.pack_store().objects_dir())
42            .field("multi_pack_index", &self.multi_pack_index)
43            .field("status", &self.status())
44            .finish()
45    }
46}
47
48impl MidxObjects {
49    /// Open MIDX reads for `store`'s `objects/` directory.
50    ///
51    /// `multi_pack_index` mirrors `core.multiPackIndex` at construction time; the store does not
52    /// read config itself.
53    ///
54    /// # Errors
55    ///
56    /// Propagates MIDX chain preparation failures other than a missing or skipped index.
57    pub fn new(store: Arc<PackStore>, multi_pack_index: bool) -> Result<Self> {
58        let packs = PackedObjects::new(Arc::clone(&store));
59        let status = if multi_pack_index {
60            classify_midx_status(store.objects_dir())?
61        } else {
62            MidxObjectsStatus::Disabled
63        };
64        Ok(Self {
65            packs,
66            multi_pack_index,
67            status: RwLock::new(status),
68        })
69    }
70
71    /// Whether MIDX-backed reads are active for this store.
72    #[must_use]
73    pub fn status(&self) -> MidxObjectsStatus {
74        self.status
75            .read()
76            .map(|g| *g)
77            .unwrap_or(MidxObjectsStatus::Unusable)
78    }
79
80    /// Whether `core.multiPackIndex` was enabled when this store was built.
81    #[must_use]
82    pub fn multi_pack_index_enabled(&self) -> bool {
83        self.multi_pack_index
84    }
85
86    /// Pack index basenames covered by the active MIDX chain (empty when inactive).
87    ///
88    /// Callers skip these indexes in [`PackedObjects`] so MIDX-covered OIDs are not scanned twice.
89    ///
90    /// # Errors
91    ///
92    /// Propagates MIDX chain load failures.
93    pub fn covered_pack_basenames(&self) -> Result<HashSet<String>> {
94        let Some(chain) = self.active_chain()? else {
95            return Ok(HashSet::new());
96        };
97        Ok(chain.covered_pack_basenames())
98    }
99
100    /// Underlying pack backend used for object payload reads.
101    #[must_use]
102    pub fn packed_objects(&self) -> &PackedObjects {
103        &self.packs
104    }
105
106    fn active_chain(&self) -> Result<Option<Arc<PreparedMidxChain>>> {
107        if self.status() != MidxObjectsStatus::Active {
108            return Ok(None);
109        }
110        match prepared_midx_chain_strict(self.packs.pack_store().objects_dir()) {
111            Ok(chain) => Ok(chain),
112            Err(Error::Midx(_)) => Ok(None),
113            Err(err) => Err(err),
114        }
115    }
116
117    fn read_via_chain(&self, oid: &ObjectId) -> Result<Option<Object>> {
118        let Some(chain) = self.active_chain()? else {
119            return Ok(None);
120        };
121        chain.try_read_object(oid, &NullDiagnostics)
122    }
123
124    fn read_info_via_chain(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
125        let Some(chain) = self.active_chain()? else {
126            return Ok(None);
127        };
128        chain.try_read_info(oid, &NullDiagnostics)
129    }
130}
131
132impl ObjectStore for MidxObjects {
133    fn hash_algo(&self) -> HashAlgo {
134        self.packs.hash_algo()
135    }
136
137    fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
138        self.read_via_chain(oid)
139    }
140
141    fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
142        self.read_info_via_chain(oid)
143    }
144
145    fn open_stream(&self, oid: &ObjectId) -> Result<Option<ObjectStream<'_>>> {
146        let Some(info) = self.read_info(oid)? else {
147            return Ok(None);
148        };
149        if let Some(stream) = self.packs.open_stream(oid)? {
150            return Ok(Some(stream));
151        }
152        let Some(obj) = self.read(oid)? else {
153            return Ok(None);
154        };
155        let size = u64::try_from(obj.data.len()).map_err(|_| {
156            Error::CorruptObject(format!("object size overflow for {}", oid.to_hex()))
157        })?;
158        Ok(Some(ObjectStream {
159            kind: info.kind,
160            size,
161            reader: Box::new(Cursor::new(obj.data)),
162        }))
163    }
164
165    fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
166        let Some(chain) = self.active_chain()? else {
167            return Ok(());
168        };
169        chain.for_each_listed_object(f)
170    }
171
172    fn lookup_prefix(&self, prefix: &str, limit: usize, out: &mut Vec<ObjectId>) -> Result<()> {
173        if prefix.len() > self.hash_algo().hex_len() {
174            return Ok(());
175        }
176        let Some(chain) = self.active_chain()? else {
177            return Ok(());
178        };
179        let prefix = ObjectIdPrefix::from_hex(prefix, self.hash_algo())?;
180        chain.lookup_prefix(&prefix, limit, out)
181    }
182
183    fn refresh(&self) -> Result<bool> {
184        if !self.multi_pack_index {
185            return Ok(false);
186        }
187        let objects_dir = self.packs.pack_store().objects_dir();
188        clear_pack_cache();
189        crate::midx::evict_midx_read_cache_for_pack_dir(&objects_dir.join("pack"));
190        let new_status = classify_midx_status(objects_dir)?;
191        let mut guard = self
192            .status
193            .write()
194            .map_err(|e| Error::Io(std::io::Error::other(e.to_string())))?;
195        let changed = *guard != new_status;
196        *guard = new_status;
197        Ok(changed)
198    }
199}
200
201fn classify_midx_status(objects_dir: &std::path::Path) -> Result<MidxObjectsStatus> {
202    match prepared_midx_chain_strict(objects_dir) {
203        Ok(Some(_)) => Ok(MidxObjectsStatus::Active),
204        Ok(None) | Err(Error::Midx(_)) | Err(Error::CorruptObject(_)) => {
205            Ok(MidxObjectsStatus::Unusable)
206        }
207        Err(err) => Err(err),
208    }
209}