gix_odb/store_impls/dynamic/
load_index.rs

1use std::{
2    collections::{BTreeMap, VecDeque},
3    ffi::OsStr,
4    ops::Deref,
5    path::{Path, PathBuf},
6    sync::{
7        atomic::{AtomicU16, Ordering},
8        Arc,
9    },
10    time::SystemTime,
11};
12
13use crate::store::{handle, types, RefreshMode};
14
15pub(crate) struct Snapshot {
16    /// Indices ready for object lookup or contains checks, ordered usually by modification data, recent ones first.
17    pub(crate) indices: Vec<handle::IndexLookup>,
18    /// A set of loose objects dbs to search once packed objects weren't found.
19    pub(crate) loose_dbs: Arc<Vec<crate::loose::Store>>,
20    /// remember what this state represents and to compare to other states.
21    pub(crate) marker: types::SlotIndexMarker,
22}
23
24mod error {
25    use std::path::PathBuf;
26
27    use gix_pack::multi_index::PackIndex;
28
29    /// Returned by [`crate::at_opts()`]
30    #[derive(thiserror::Error, Debug)]
31    #[allow(missing_docs)]
32    pub enum Error {
33        #[error("The objects directory at '{0}' is not an accessible directory")]
34        Inaccessible(PathBuf),
35        #[error(transparent)]
36        Io(#[from] std::io::Error),
37        #[error(transparent)]
38        Alternate(#[from] crate::alternate::Error),
39        #[error("The slotmap turned out to be too small with {} entries, would need {} more", .current, .needed)]
40        InsufficientSlots { current: usize, needed: usize },
41        /// The problem here is that some logic assumes that more recent generations are higher than previous ones. If we would overflow,
42        /// we would break that invariant which can lead to the wrong object from being returned. It would probably be super rare, but…
43        /// let's not risk it.
44        #[error(
45            "Would have overflown amount of max possible generations of {}",
46            super::Generation::MAX
47        )]
48        GenerationOverflow,
49        #[error("Cannot numerically handle more than {limit} packs in a single multi-pack index, got {actual} in file {index_path:?}")]
50        TooManyPacksInMultiIndex {
51            actual: PackIndex,
52            limit: PackIndex,
53            index_path: PathBuf,
54        },
55    }
56}
57
58pub use error::Error;
59
60use crate::store::types::{Generation, IndexAndPacks, MutableIndexAndPack, PackId, SlotMapIndex};
61
62impl super::Store {
63    /// Load all indices, refreshing from disk only if needed.
64    pub(crate) fn load_all_indices(&self) -> Result<Snapshot, Error> {
65        let mut snapshot = self.collect_snapshot();
66        while let Some(new_snapshot) = self.load_one_index(RefreshMode::Never, snapshot.marker)? {
67            snapshot = new_snapshot;
68        }
69        Ok(snapshot)
70    }
71
72    /// If `None` is returned, there is new indices and the caller should give up. This is a possibility even if it's allowed to refresh
73    /// as here might be no change to pick up.
74    pub(crate) fn load_one_index(
75        &self,
76        refresh_mode: RefreshMode,
77        marker: types::SlotIndexMarker,
78    ) -> Result<Option<Snapshot>, Error> {
79        let index = self.index.load();
80        if !index.is_initialized() {
81            return self.consolidate_with_disk_state(true /* needs_init */, false /*load one new index*/);
82        }
83
84        if marker.generation != index.generation || marker.state_id != index.state_id() {
85            // We have a more recent state already, provide it.
86            Ok(Some(self.collect_snapshot()))
87        } else {
88            // always compare to the latest state
89            // Nothing changed in the meantime, try to load another index…
90            if self.load_next_index(index) {
91                Ok(Some(self.collect_snapshot()))
92            } else {
93                // …and if that didn't yield anything new consider refreshing our disk state.
94                match refresh_mode {
95                    RefreshMode::Never => Ok(None),
96                    RefreshMode::AfterAllIndicesLoaded => {
97                        self.consolidate_with_disk_state(false /* needs init */, true /*load one new index*/)
98                    }
99                }
100            }
101        }
102    }
103
104    /// load a new index (if not yet loaded), and return true if one was indeed loaded (leading to a `state_id()` change) of the current index.
105    /// Note that interacting with the slot-map is inherently racy and we have to deal with it, being conservative in what we even try to load
106    /// as our index might already be out-of-date as we try to use it to learn what's next.
107    fn load_next_index(&self, mut index: arc_swap::Guard<Arc<SlotMapIndex>>) -> bool {
108        'retry_with_changed_index: loop {
109            let previous_state_id = index.state_id();
110            'retry_with_next_slot_index: loop {
111                match index
112                    .next_index_to_load
113                    .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| {
114                        (current != index.slot_indices.len()).then_some(current + 1)
115                    }) {
116                    Ok(slot_map_index) => {
117                        // This slot-map index is in bounds and was only given to us.
118                        let _ongoing_operation = IncOnNewAndDecOnDrop::new(&index.num_indices_currently_being_loaded);
119                        let slot = &self.files[index.slot_indices[slot_map_index]];
120                        let _lock = slot.write.lock();
121                        if slot.generation.load(Ordering::SeqCst) > index.generation {
122                            // There is a disk consolidation in progress which just overwrote a slot that could be disposed with some other
123                            // index, one we didn't intend to load.
124                            // Continue with the next slot index in the hope there is something else we can do…
125                            continue 'retry_with_next_slot_index;
126                        }
127                        let mut bundle = slot.files.load_full();
128                        let bundle_mut = Arc::make_mut(&mut bundle);
129                        if let Some(files) = bundle_mut.as_mut() {
130                            // these are always expected to be set, unless somebody raced us. We handle this later by retrying.
131                            let res = {
132                                let res = files.load_index(self.object_hash);
133                                slot.files.store(bundle);
134                                index.loaded_indices.fetch_add(1, Ordering::SeqCst);
135                                res
136                            };
137                            match res {
138                                Ok(_) => {
139                                    break 'retry_with_next_slot_index;
140                                }
141                                Err(_err) => {
142                                    gix_features::trace::error!(err=?_err, "Failed to load index file - some objects may seem to not exist");
143                                    continue 'retry_with_next_slot_index;
144                                }
145                            }
146                        }
147                    }
148                    Err(_nothing_more_to_load) => {
149                        // There can be contention as many threads start working at the same time and take all the
150                        // slots to load indices for. Some threads might just be left-over and have to wait for something
151                        // to change.
152                        // TODO: potentially hot loop - could this be a condition variable?
153                        // This is a timing-based fix for the case that the `num_indices_being_loaded` isn't yet incremented,
154                        // and we might break out here without actually waiting for the loading operation. Then we'd fail to
155                        // observe a change and the underlying handler would not have all the indices it needs at its disposal.
156                        // Yielding means we will definitely loose enough time to observe the ongoing operation,
157                        // or its effects.
158                        std::thread::yield_now();
159                        while index.num_indices_currently_being_loaded.load(Ordering::SeqCst) != 0 {
160                            std::thread::yield_now();
161                        }
162                        break 'retry_with_next_slot_index;
163                    }
164                }
165            }
166            if previous_state_id == index.state_id() {
167                let potentially_new_index = self.index.load();
168                if std::ptr::eq(Arc::as_ptr(&potentially_new_index), Arc::as_ptr(&index)) {
169                    // There isn't a new index with which to retry the whole ordeal, so nothing could be done here.
170                    return false;
171                } else {
172                    // the index changed, worth trying again
173                    index = potentially_new_index;
174                    continue 'retry_with_changed_index;
175                }
176            } else {
177                // something inarguably changed, probably an index was loaded. 'probably' because we consider failed loads valid attempts,
178                // even they don't change anything for the caller which would then do a round for nothing.
179                return true;
180            }
181        }
182    }
183
184    /// refresh and possibly clear out our existing data structures, causing all pack ids to be invalidated.
185    /// `load_new_index` is an optimization to at least provide one newly loaded pack after refreshing the slot map.
186    pub(crate) fn consolidate_with_disk_state(
187        &self,
188        needs_init: bool,
189        load_new_index: bool,
190    ) -> Result<Option<Snapshot>, Error> {
191        let index = self.index.load();
192        let previous_index_state = Arc::as_ptr(&index) as usize;
193
194        // IMPORTANT: get a lock after we recorded the previous state.
195        let write = self.write.lock();
196        let objects_directory = &self.path;
197
198        // Now we know the index isn't going to change anymore, even though threads might still load indices in the meantime.
199        let index = self.index.load();
200        if previous_index_state != Arc::as_ptr(&index) as usize {
201            // Someone else took the look before and changed the index. Return it without doing any additional work.
202            return Ok(Some(self.collect_snapshot()));
203        }
204
205        let was_uninitialized = !index.is_initialized();
206
207        // We might not be able to detect by pointer if the state changed, as this itself is racy. So we keep track of double-initialization
208        // using a flag, which means that if `needs_init` was true we saw the index uninitialized once, but now that we are here it's
209        // initialized meaning that somebody was faster, and we couldn't detect it by comparisons to the index.
210        // If so, make sure we collect the snapshot instead of returning None in case nothing actually changed, which is likely with a
211        // race like this.
212        if !was_uninitialized && needs_init {
213            return Ok(Some(self.collect_snapshot()));
214        }
215        self.num_disk_state_consolidation.fetch_add(1, Ordering::Relaxed);
216
217        let db_paths: Vec<_> = std::iter::once(objects_directory.to_owned())
218            .chain(crate::alternate::resolve(objects_directory.clone(), &self.current_dir)?)
219            .collect();
220
221        // turn db paths into loose object databases. Reuse what's there, but only if it is in the right order.
222        let loose_dbs = if was_uninitialized
223            || db_paths.len() != index.loose_dbs.len()
224            || db_paths
225                .iter()
226                .zip(index.loose_dbs.iter().map(|ldb| &ldb.path))
227                .any(|(lhs, rhs)| lhs != rhs)
228        {
229            Arc::new(
230                db_paths
231                    .iter()
232                    .map(|path| crate::loose::Store::at(path, self.object_hash))
233                    .collect::<Vec<_>>(),
234            )
235        } else {
236            Arc::clone(&index.loose_dbs)
237        };
238
239        let indices_by_modification_time = Self::collect_indices_and_mtime_sorted_by_size(
240            db_paths,
241            index.slot_indices.len().into(),
242            self.use_multi_pack_index.then_some(self.object_hash),
243        )?;
244        let mut idx_by_index_path: BTreeMap<_, _> = index
245            .slot_indices
246            .iter()
247            .filter_map(|&idx| {
248                let f = &self.files[idx];
249                Option::as_ref(&f.files.load()).map(|f| (f.index_path().to_owned(), idx))
250            })
251            .collect();
252
253        let mut new_slot_map_indices = Vec::new(); // these indices into the slot map still exist there/didn't change
254        let mut index_paths_to_add = was_uninitialized
255            .then(|| VecDeque::with_capacity(indices_by_modification_time.len()))
256            .unwrap_or_default();
257
258        // Figure out this number based on what we see while handling the existing indices
259        let mut num_loaded_indices = 0;
260        for (index_info, mtime) in indices_by_modification_time.into_iter().map(|(a, b, _)| (a, b)) {
261            match idx_by_index_path.remove(index_info.path()) {
262                Some(slot_idx) => {
263                    let slot = &self.files[slot_idx];
264                    let files_guard = slot.files.load();
265                    let files =
266                        Option::as_ref(&files_guard).expect("slot is set or we wouldn't know it points to this file");
267                    if index_info.is_multi_index() && files.mtime() != mtime {
268                        // we have a changed multi-pack index. We can't just change the existing slot as it may alter slot indices
269                        // that are currently available. Instead, we have to move what's there into a new slot, along with the changes,
270                        // and later free the slot or dispose of the index in the slot (like we do for removed/missing files).
271                        index_paths_to_add.push_back((index_info, mtime, Some(slot_idx)));
272                        // If the current slot is loaded, the soon-to-be copied multi-index path will be loaded as well.
273                        if files.index_is_loaded() {
274                            num_loaded_indices += 1;
275                        }
276                    } else {
277                        // packs and indices are immutable, so no need to check modification times. Unchanged multi-pack indices also
278                        // are handled like this just to be sure they are in the desired state. For these, the only way this could happen
279                        // is if somebody deletes and then puts back
280                        if Self::assure_slot_matches_index(&write, slot, index_info, mtime, index.generation) {
281                            num_loaded_indices += 1;
282                        }
283                        new_slot_map_indices.push(slot_idx);
284                    }
285                }
286                None => index_paths_to_add.push_back((index_info, mtime, None)),
287            }
288        }
289        let needs_stable_indices = self.maintain_stable_indices(&write);
290
291        let mut next_possibly_free_index = index
292            .slot_indices
293            .iter()
294            .max()
295            .map_or(0, |idx| (idx + 1) % self.files.len());
296        let mut num_indices_checked = 0;
297        let mut needs_generation_change = false;
298        let mut slot_indices_to_remove: Vec<_> = idx_by_index_path.into_values().collect();
299        while let Some((mut index_info, mtime, move_from_slot_idx)) = index_paths_to_add.pop_front() {
300            'increment_slot_index: loop {
301                if num_indices_checked == self.files.len() {
302                    return Err(Error::InsufficientSlots {
303                        current: self.files.len(),
304                        needed: index_paths_to_add.len() + 1, /*the one currently popped off*/
305                    });
306                }
307                // Don't allow duplicate indicates, we need a 1:1 mapping.
308                if new_slot_map_indices.contains(&next_possibly_free_index) {
309                    next_possibly_free_index = (next_possibly_free_index + 1) % self.files.len();
310                    num_indices_checked += 1;
311                    continue 'increment_slot_index;
312                }
313                let slot_index = next_possibly_free_index;
314                let slot = &self.files[slot_index];
315                next_possibly_free_index = (next_possibly_free_index + 1) % self.files.len();
316                num_indices_checked += 1;
317                match move_from_slot_idx {
318                    Some(move_from_slot_idx) => {
319                        debug_assert!(index_info.is_multi_index(), "only set for multi-pack indices");
320                        if slot_index == move_from_slot_idx {
321                            // don't try to move onto ourselves
322                            continue 'increment_slot_index;
323                        }
324                        match Self::try_set_index_slot(
325                            &write,
326                            slot,
327                            index_info,
328                            mtime,
329                            index.generation,
330                            needs_stable_indices,
331                        ) {
332                            Ok(dest_was_empty) => {
333                                slot_indices_to_remove.push(move_from_slot_idx);
334                                new_slot_map_indices.push(slot_index);
335                                // To avoid handling out the wrong pack (due to reassigned pack ids), declare this a new generation.
336                                if !dest_was_empty {
337                                    needs_generation_change = true;
338                                }
339                                break 'increment_slot_index;
340                            }
341                            Err(unused_index_info) => index_info = unused_index_info,
342                        }
343                    }
344                    None => {
345                        match Self::try_set_index_slot(
346                            &write,
347                            slot,
348                            index_info,
349                            mtime,
350                            index.generation,
351                            needs_stable_indices,
352                        ) {
353                            Ok(dest_was_empty) => {
354                                new_slot_map_indices.push(slot_index);
355                                if !dest_was_empty {
356                                    needs_generation_change = true;
357                                }
358                                break 'increment_slot_index;
359                            }
360                            Err(unused_index_info) => index_info = unused_index_info,
361                        }
362                    }
363                }
364                // This isn't racy as it's only us who can change the Option::Some/None state of a slot.
365            }
366        }
367        assert_eq!(
368            index_paths_to_add.len(),
369            0,
370            "By this time we have assigned all new files to slots"
371        );
372
373        let generation = if needs_generation_change {
374            index.generation.checked_add(1).ok_or(Error::GenerationOverflow)?
375        } else {
376            index.generation
377        };
378        let index_unchanged = index.slot_indices == new_slot_map_indices;
379        if generation != index.generation {
380            assert!(
381                !index_unchanged,
382                "if the generation changed, the slot index must have changed for sure"
383            );
384        }
385        if !index_unchanged || loose_dbs != index.loose_dbs {
386            let new_index = Arc::new(SlotMapIndex {
387                slot_indices: new_slot_map_indices,
388                loose_dbs,
389                generation,
390                // if there was a prior generation, some indices might already be loaded. But we deal with it by trying to load the next index then,
391                // until we find one.
392                next_index_to_load: index_unchanged
393                    .then(|| Arc::clone(&index.next_index_to_load))
394                    .unwrap_or_default(),
395                loaded_indices: if index_unchanged {
396                    Arc::clone(&index.loaded_indices)
397                } else {
398                    Arc::new(num_loaded_indices.into())
399                },
400                num_indices_currently_being_loaded: Default::default(),
401            });
402            self.index.store(new_index);
403        }
404
405        // deleted items - remove their slots AFTER we have set the new index if we may alter indices, otherwise we only declare them garbage.
406        // removing slots may cause pack loading to fail, and they will then reload their indices.
407        for slot in slot_indices_to_remove.into_iter().map(|idx| &self.files[idx]) {
408            let _lock = slot.write.lock();
409            let mut files = slot.files.load_full();
410            let files_mut = Arc::make_mut(&mut files);
411            if needs_stable_indices {
412                if let Some(files) = files_mut.as_mut() {
413                    files.trash();
414                    // generation stays the same, as it's the same value still but scheduled for eventual removal.
415                }
416            } else {
417                // set the generation before we actually change the value, otherwise readers of old generations could observe the new one.
418                // We rather want them to turn around here and update their index, which, by that time, might actually already be available.
419                // If not, they would fail unable to load a pack or index they need, but that's preferred over returning wrong objects.
420                // Safety: can't race as we hold the lock, have to set the generation beforehand to help avoid others to observe the value.
421                slot.generation.store(generation, Ordering::SeqCst);
422                *files_mut = None;
423            }
424            slot.files.store(files);
425        }
426
427        let new_index = self.index.load();
428        Ok(if index.state_id() == new_index.state_id() {
429            // there was no change, and nothing was loaded in the meantime, reflect that in the return value to not get into loops.
430            None
431        } else {
432            if load_new_index {
433                self.load_next_index(new_index);
434            }
435            Some(self.collect_snapshot())
436        })
437    }
438
439    pub(crate) fn collect_indices_and_mtime_sorted_by_size(
440        db_paths: Vec<PathBuf>,
441        initial_capacity: Option<usize>,
442        multi_pack_index_object_hash: Option<gix_hash::Kind>,
443    ) -> Result<Vec<(Either, SystemTime, u64)>, Error> {
444        let mut indices_by_modification_time = Vec::with_capacity(initial_capacity.unwrap_or_default());
445        for db_path in db_paths {
446            let packs = db_path.join("pack");
447            let entries = match std::fs::read_dir(packs) {
448                Ok(e) => e,
449                Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
450                Err(err) => return Err(err.into()),
451            };
452            let indices = entries
453                .filter_map(Result::ok)
454                .filter_map(|e| e.metadata().map(|md| (e.path(), md)).ok())
455                .filter(|(_, md)| md.file_type().is_file())
456                .filter(|(p, _)| {
457                    let ext = p.extension();
458                    (ext == Some(OsStr::new("idx")) && p.with_extension("pack").is_file())
459                        || (multi_pack_index_object_hash.is_some() && ext.is_none() && is_multipack_index(p))
460                })
461                .map(|(p, md)| md.modified().map_err(Error::from).map(|mtime| (p, mtime, md.len())))
462                .collect::<Result<Vec<_>, _>>()?;
463
464            let multi_index_info = multi_pack_index_object_hash
465                .and_then(|hash| {
466                    indices.iter().find_map(|(p, a, b)| {
467                        is_multipack_index(p)
468                            .then(|| {
469                                // we always open the multi-pack here to be able to remove indices
470                                gix_pack::multi_index::File::at(p)
471                                    .ok()
472                                    .filter(|midx| midx.object_hash() == hash)
473                                    .map(|midx| (midx, *a, *b))
474                            })
475                            .flatten()
476                            .map(|t| {
477                                if t.0.num_indices() > PackId::max_packs_in_multi_index() {
478                                    Err(Error::TooManyPacksInMultiIndex {
479                                        index_path: p.to_owned(),
480                                        actual: t.0.num_indices(),
481                                        limit: PackId::max_packs_in_multi_index(),
482                                    })
483                                } else {
484                                    Ok(t)
485                                }
486                            })
487                    })
488                })
489                .transpose()?;
490            if let Some((multi_index, mtime, flen)) = multi_index_info {
491                let index_names_in_multi_index: Vec<_> = multi_index.index_names().iter().map(AsRef::as_ref).collect();
492                let mut indices_not_in_multi_index: Vec<(Either, _, _)> = indices
493                    .into_iter()
494                    .filter_map(|(path, a, b)| {
495                        (path != multi_index.path()
496                            && !index_names_in_multi_index
497                                .contains(&Path::new(path.file_name().expect("file name present"))))
498                        .then_some((Either::IndexPath(path), a, b))
499                    })
500                    .collect();
501                indices_not_in_multi_index.insert(0, (Either::MultiIndexFile(Arc::new(multi_index)), mtime, flen));
502                indices_by_modification_time.extend(indices_not_in_multi_index);
503            } else {
504                indices_by_modification_time.extend(
505                    indices
506                        .into_iter()
507                        .filter_map(|(p, a, b)| (!is_multipack_index(&p)).then_some((Either::IndexPath(p), a, b))),
508                );
509            }
510        }
511        // Unlike libgit2, do not sort by modification date, but by size and put the biggest indices first. That way
512        // the chance to hit an object should be higher. We leave it to the handle to sort by LRU.
513        // Git itself doesn't change the order which may save time, but we want it to be stable which also helps some tests.
514        // NOTE: this will work well for well-packed repos or those using geometric repacking, but force us to open a lot
515        //       of files when dealing with new objects, as there is no notion of recency here as would be with unmaintained
516        //       repositories. Different algorithms should be provided, like newest packs first, and possibly a mix of both
517        //       with big packs first, then sorting by recency for smaller packs.
518        //       We also want to implement `fetch.unpackLimit` to alleviate this issue a little.
519        indices_by_modification_time.sort_by(|l, r| l.2.cmp(&r.2).reverse());
520        Ok(indices_by_modification_time)
521    }
522
523    /// returns `Ok(dest_slot_was_empty)` if the copy could happen because dest-slot was actually free or disposable.
524    #[allow(clippy::too_many_arguments)]
525    fn try_set_index_slot(
526        lock: &parking_lot::MutexGuard<'_, ()>,
527        dest_slot: &MutableIndexAndPack,
528        index_info: Either,
529        mtime: SystemTime,
530        current_generation: Generation,
531        needs_stable_indices: bool,
532    ) -> Result<bool, Either> {
533        let (dest_slot_was_empty, generation) = match &**dest_slot.files.load() {
534            Some(bundle) => {
535                if bundle.index_path() == index_info.path() || (bundle.is_disposable() && needs_stable_indices) {
536                    // it might be possible to see ourselves in case all slots are taken, but there are still a few more destination
537                    // slots to look for.
538                    return Err(index_info);
539                }
540                // Since we overwrite an existing slot, we have to increment the generation to prevent anyone from trying to use it while
541                // before we are replacing it with a different value.
542                // In detail:
543                // We need to declare this to be the future to avoid anything in that slot to be returned to people who
544                // last saw the old state. They will then try to get a new index which by that time, might be happening
545                // in time so they get the latest one. If not, they will probably get into the same situation again until
546                // it finally succeeds. Alternatively, the object will be reported unobtainable, but at least it won't return
547                // some other object.
548                (false, current_generation + 1)
549            }
550            None => {
551                // For multi-pack indices:
552                //   Do NOT copy the packs over, they need to be reopened to get the correct pack id matching the new slot map index.
553                //   If we are allowed to delete the original, and nobody has the pack referenced, it is closed which is preferred.
554                //   Thus we simply always start new with packs in multi-pack indices.
555                //   In the worst case this could mean duplicate file handle usage though as the old and the new index can't share
556                //   packs due to the intrinsic id.
557                //   Note that the ID is used for cache access, too, so it must be unique. It must also be mappable from pack-id to slotmap id.
558                (true, current_generation)
559            }
560        };
561        Self::set_slot_to_index(lock, dest_slot, index_info, mtime, generation);
562        Ok(dest_slot_was_empty)
563    }
564
565    fn set_slot_to_index(
566        _lock: &parking_lot::MutexGuard<'_, ()>,
567        slot: &MutableIndexAndPack,
568        index_info: Either,
569        mtime: SystemTime,
570        generation: Generation,
571    ) {
572        let _lock = slot.write.lock();
573        let mut files = slot.files.load_full();
574        let files_mut = Arc::make_mut(&mut files);
575        // set the generation before we actually change the value, otherwise readers of old generations could observe the new one.
576        // We rather want them to turn around here and update their index, which, by that time, might actually already be available.
577        // If not, they would fail unable to load a pack or index they need, but that's preferred over returning wrong objects.
578        // Safety: can't race as we hold the lock, have to set the generation beforehand to help avoid others to observe the value.
579        slot.generation.store(generation, Ordering::SeqCst);
580        *files_mut = Some(index_info.into_index_and_packs(mtime));
581        slot.files.store(files);
582    }
583
584    /// Returns true if the index was left in a loaded state.
585    fn assure_slot_matches_index(
586        _lock: &parking_lot::MutexGuard<'_, ()>,
587        slot: &MutableIndexAndPack,
588        index_info: Either,
589        mtime: SystemTime,
590        current_generation: Generation,
591    ) -> bool {
592        match Option::as_ref(&slot.files.load()) {
593            Some(bundle) => {
594                assert_eq!(
595                    bundle.index_path(),
596                    index_info.path(),
597                    "Parallel writers cannot change the file the slot points to."
598                );
599                if bundle.is_disposable() {
600                    // put it into the correct mode, it's now available for sure so should not be missing or garbage.
601                    // The latter can happen if files are removed and put back for some reason, but we should definitely
602                    // have them in a decent state now that we know/think they are there.
603                    let _lock = slot.write.lock();
604                    let mut files = slot.files.load_full();
605                    let files_mut = Arc::make_mut(&mut files)
606                        .as_mut()
607                        .expect("BUG: cannot change from something to nothing, would be race");
608                    files_mut.put_back();
609                    debug_assert_eq!(
610                        files_mut.mtime(),
611                        mtime,
612                        "BUG: we can only put back files that didn't obviously change"
613                    );
614                    // Safety: can't race as we hold the lock, must be set before replacing the data.
615                    // NOTE that we don't change the generation as it's still the very same index we talk about, it doesn't change
616                    // identity.
617                    slot.generation.store(current_generation, Ordering::SeqCst);
618                    slot.files.store(files);
619                } else {
620                    // it's already in the correct state, either loaded or unloaded.
621                }
622                bundle.index_is_loaded()
623            }
624            None => {
625                unreachable!("BUG: a slot can never be deleted if we have it recorded in the index WHILE changing said index. There shouldn't be a race")
626            }
627        }
628    }
629
630    /// Stability means that indices returned by this API will remain valid.
631    /// Without that constraint, we may unload unused packs and indices, and may rebuild the slotmap index.
632    ///
633    /// Note that this must be called with a lock to the relevant state held to assure these values don't change while
634    /// we are working on said index.
635    fn maintain_stable_indices(&self, _guard: &parking_lot::MutexGuard<'_, ()>) -> bool {
636        self.num_handles_stable.load(Ordering::SeqCst) > 0
637    }
638
639    pub(crate) fn collect_snapshot(&self) -> Snapshot {
640        // We don't observe changes-on-disk in our 'wait-for-load' loop.
641        // That loop is meant to help assure the marker (which includes the amount of loaded indices) matches
642        // the actual amount of indices we collect.
643        let index = self.index.load();
644        loop {
645            if index.num_indices_currently_being_loaded.deref().load(Ordering::SeqCst) != 0 {
646                std::thread::yield_now();
647                continue;
648            }
649            let marker = index.marker();
650            let indices = if index.is_initialized() {
651                index
652                    .slot_indices
653                    .iter()
654                    .map(|idx| (*idx, &self.files[*idx]))
655                    .filter_map(|(id, file)| {
656                        let lookup = match (**file.files.load()).as_ref()? {
657                            types::IndexAndPacks::Index(bundle) => handle::SingleOrMultiIndex::Single {
658                                index: bundle.index.loaded()?.clone(),
659                                data: bundle.data.loaded().cloned(),
660                            },
661                            types::IndexAndPacks::MultiIndex(multi) => handle::SingleOrMultiIndex::Multi {
662                                index: multi.multi_index.loaded()?.clone(),
663                                data: multi.data.iter().map(|f| f.loaded().cloned()).collect(),
664                            },
665                        };
666                        handle::IndexLookup { file: lookup, id }.into()
667                    })
668                    .collect()
669            } else {
670                Vec::new()
671            };
672
673            return Snapshot {
674                indices,
675                loose_dbs: Arc::clone(&index.loose_dbs),
676                marker,
677            };
678        }
679    }
680}
681
682// Outside of this method we will never assign new slot indices.
683fn is_multipack_index(path: &Path) -> bool {
684    path.file_name() == Some(OsStr::new("multi-pack-index"))
685}
686
687struct IncOnNewAndDecOnDrop<'a>(&'a AtomicU16);
688impl<'a> IncOnNewAndDecOnDrop<'a> {
689    pub fn new(v: &'a AtomicU16) -> Self {
690        v.fetch_add(1, Ordering::SeqCst);
691        Self(v)
692    }
693}
694impl Drop for IncOnNewAndDecOnDrop<'_> {
695    fn drop(&mut self) {
696        self.0.fetch_sub(1, Ordering::SeqCst);
697    }
698}
699
700pub(crate) enum Either {
701    IndexPath(PathBuf),
702    MultiIndexFile(Arc<gix_pack::multi_index::File>),
703}
704
705impl Either {
706    fn path(&self) -> &Path {
707        match self {
708            Either::IndexPath(p) => p,
709            Either::MultiIndexFile(f) => f.path(),
710        }
711    }
712
713    fn into_index_and_packs(self, mtime: SystemTime) -> IndexAndPacks {
714        match self {
715            Either::IndexPath(path) => IndexAndPacks::new_single(path, mtime),
716            Either::MultiIndexFile(file) => IndexAndPacks::new_multi_from_open_file(file, mtime),
717        }
718    }
719
720    fn is_multi_index(&self) -> bool {
721        matches!(self, Either::MultiIndexFile(_))
722    }
723}
724
725impl Eq for Either {}
726
727impl PartialEq<Self> for Either {
728    fn eq(&self, other: &Self) -> bool {
729        self.path().eq(other.path())
730    }
731}
732
733impl PartialOrd<Self> for Either {
734    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
735        Some(self.path().cmp(other.path()))
736    }
737}
738
739impl Ord for Either {
740    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
741        self.path().cmp(other.path())
742    }
743}