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 = if was_uninitialized {
255 VecDeque::with_capacity(indices_by_modification_time.len())
256 } else {
257 Default::default()
258 };
259
260 // Figure out this number based on what we see while handling the existing indices
261 let mut num_loaded_indices = 0;
262 for (index_info, mtime) in indices_by_modification_time.into_iter().map(|(a, b, _)| (a, b)) {
263 match idx_by_index_path.remove(index_info.path()) {
264 Some(slot_idx) => {
265 let slot = &self.files[slot_idx];
266 let files_guard = slot.files.load();
267 let files =
268 Option::as_ref(&files_guard).expect("slot is set or we wouldn't know it points to this file");
269 if index_info.is_multi_index() && files.mtime() != mtime {
270 // we have a changed multi-pack index. We can't just change the existing slot as it may alter slot indices
271 // that are currently available. Instead, we have to move what's there into a new slot, along with the changes,
272 // and later free the slot or dispose of the index in the slot (like we do for removed/missing files).
273 index_paths_to_add.push_back((index_info, mtime, Some(slot_idx)));
274 // If the current slot is loaded, the soon-to-be copied multi-index path will be loaded as well.
275 if files.index_is_loaded() {
276 num_loaded_indices += 1;
277 }
278 } else {
279 // packs and indices are immutable, so no need to check modification times. Unchanged multi-pack indices also
280 // are handled like this just to be sure they are in the desired state. For these, the only way this could happen
281 // is if somebody deletes and then puts back
282 if Self::assure_slot_matches_index(&write, slot, index_info, mtime, index.generation) {
283 num_loaded_indices += 1;
284 }
285 new_slot_map_indices.push(slot_idx);
286 }
287 }
288 None => index_paths_to_add.push_back((index_info, mtime, None)),
289 }
290 }
291 let needs_stable_indices = self.maintain_stable_indices(&write);
292
293 let mut next_possibly_free_index = index
294 .slot_indices
295 .iter()
296 .max()
297 .map_or(0, |idx| (idx + 1) % self.files.len());
298 let mut num_indices_checked = 0;
299 let mut needs_generation_change = false;
300 let mut slot_indices_to_remove: Vec<_> = idx_by_index_path.into_values().collect();
301 while let Some((mut index_info, mtime, move_from_slot_idx)) = index_paths_to_add.pop_front() {
302 'increment_slot_index: loop {
303 if num_indices_checked == self.files.len() {
304 return Err(Error::InsufficientSlots {
305 current: self.files.len(),
306 needed: index_paths_to_add.len() + 1, /*the one currently popped off*/
307 });
308 }
309 // Don't allow duplicate indicates, we need a 1:1 mapping.
310 if new_slot_map_indices.contains(&next_possibly_free_index) {
311 next_possibly_free_index = (next_possibly_free_index + 1) % self.files.len();
312 num_indices_checked += 1;
313 continue 'increment_slot_index;
314 }
315 let slot_index = next_possibly_free_index;
316 let slot = &self.files[slot_index];
317 next_possibly_free_index = (next_possibly_free_index + 1) % self.files.len();
318 num_indices_checked += 1;
319 match move_from_slot_idx {
320 Some(move_from_slot_idx) => {
321 debug_assert!(index_info.is_multi_index(), "only set for multi-pack indices");
322 if slot_index == move_from_slot_idx {
323 // don't try to move onto ourselves
324 continue 'increment_slot_index;
325 }
326 match Self::try_set_index_slot(
327 &write,
328 slot,
329 index_info,
330 mtime,
331 index.generation,
332 needs_stable_indices,
333 ) {
334 Ok(dest_was_empty) => {
335 slot_indices_to_remove.push(move_from_slot_idx);
336 new_slot_map_indices.push(slot_index);
337 // To avoid handling out the wrong pack (due to reassigned pack ids), declare this a new generation.
338 if !dest_was_empty {
339 needs_generation_change = true;
340 }
341 break 'increment_slot_index;
342 }
343 Err(unused_index_info) => index_info = unused_index_info,
344 }
345 }
346 None => {
347 match Self::try_set_index_slot(
348 &write,
349 slot,
350 index_info,
351 mtime,
352 index.generation,
353 needs_stable_indices,
354 ) {
355 Ok(dest_was_empty) => {
356 new_slot_map_indices.push(slot_index);
357 if !dest_was_empty {
358 needs_generation_change = true;
359 }
360 break 'increment_slot_index;
361 }
362 Err(unused_index_info) => index_info = unused_index_info,
363 }
364 }
365 }
366 // This isn't racy as it's only us who can change the Option::Some/None state of a slot.
367 }
368 }
369 assert_eq!(
370 index_paths_to_add.len(),
371 0,
372 "By this time we have assigned all new files to slots"
373 );
374
375 let generation = if needs_generation_change {
376 index.generation.checked_add(1).ok_or(Error::GenerationOverflow)?
377 } else {
378 index.generation
379 };
380 let index_unchanged = index.slot_indices == new_slot_map_indices;
381 if generation != index.generation {
382 assert!(
383 !index_unchanged,
384 "if the generation changed, the slot index must have changed for sure"
385 );
386 }
387 if !index_unchanged || loose_dbs != index.loose_dbs {
388 let new_index = Arc::new(SlotMapIndex {
389 slot_indices: new_slot_map_indices,
390 loose_dbs,
391 generation,
392 // 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,
393 // until we find one.
394 next_index_to_load: if index_unchanged {
395 Arc::clone(&index.next_index_to_load)
396 } else {
397 Default::default()
398 },
399 loaded_indices: if index_unchanged {
400 Arc::clone(&index.loaded_indices)
401 } else {
402 Arc::new(num_loaded_indices.into())
403 },
404 num_indices_currently_being_loaded: Default::default(),
405 });
406 self.index.store(new_index);
407 }
408
409 // deleted items - remove their slots AFTER we have set the new index if we may alter indices, otherwise we only declare them garbage.
410 // removing slots may cause pack loading to fail, and they will then reload their indices.
411 for slot in slot_indices_to_remove.into_iter().map(|idx| &self.files[idx]) {
412 let _lock = slot.write.lock();
413 let mut files = slot.files.load_full();
414 let files_mut = Arc::make_mut(&mut files);
415 if needs_stable_indices {
416 if let Some(files) = files_mut.as_mut() {
417 files.trash();
418 // generation stays the same, as it's the same value still but scheduled for eventual removal.
419 }
420 } else {
421 // set the generation before we actually change the value, otherwise readers of old generations could observe the new one.
422 // We rather want them to turn around here and update their index, which, by that time, might actually already be available.
423 // If not, they would fail unable to load a pack or index they need, but that's preferred over returning wrong objects.
424 // Safety: can't race as we hold the lock, have to set the generation beforehand to help avoid others to observe the value.
425 slot.generation.store(generation, Ordering::SeqCst);
426 *files_mut = None;
427 }
428 slot.files.store(files);
429 }
430
431 let new_index = self.index.load();
432 Ok(if index.state_id() == new_index.state_id() {
433 // there was no change, and nothing was loaded in the meantime, reflect that in the return value to not get into loops.
434 None
435 } else {
436 if load_new_index {
437 self.load_next_index(new_index);
438 }
439 Some(self.collect_snapshot())
440 })
441 }
442
443 pub(crate) fn collect_indices_and_mtime_sorted_by_size(
444 db_paths: Vec<PathBuf>,
445 initial_capacity: Option<usize>,
446 multi_pack_index_object_hash: Option<gix_hash::Kind>,
447 ) -> Result<Vec<(Either, SystemTime, u64)>, Error> {
448 let mut indices_by_modification_time = Vec::with_capacity(initial_capacity.unwrap_or_default());
449 for db_path in db_paths {
450 let packs = db_path.join("pack");
451 let entries = match std::fs::read_dir(packs) {
452 Ok(e) => e,
453 Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
454 Err(err) => return Err(err.into()),
455 };
456 let indices = entries
457 .filter_map(Result::ok)
458 .filter_map(|e| e.metadata().map(|md| (e.path(), md)).ok())
459 .filter(|(_, md)| md.file_type().is_file())
460 .filter(|(p, _)| {
461 let ext = p.extension();
462 (ext == Some(OsStr::new("idx")) && p.with_extension("pack").is_file())
463 || (multi_pack_index_object_hash.is_some() && ext.is_none() && is_multipack_index(p))
464 })
465 .map(|(p, md)| md.modified().map_err(Error::from).map(|mtime| (p, mtime, md.len())))
466 .collect::<Result<Vec<_>, _>>()?;
467
468 let multi_index_info = multi_pack_index_object_hash
469 .and_then(|hash| {
470 indices.iter().find_map(|(p, a, b)| {
471 is_multipack_index(p)
472 .then(|| {
473 // we always open the multi-pack here to be able to remove indices
474 gix_pack::multi_index::File::at(p)
475 .ok()
476 .filter(|midx| midx.object_hash() == hash)
477 .map(|midx| (midx, *a, *b))
478 })
479 .flatten()
480 .map(|t| {
481 if t.0.num_indices() > PackId::max_packs_in_multi_index() {
482 Err(Error::TooManyPacksInMultiIndex {
483 index_path: p.to_owned(),
484 actual: t.0.num_indices(),
485 limit: PackId::max_packs_in_multi_index(),
486 })
487 } else {
488 Ok(t)
489 }
490 })
491 })
492 })
493 .transpose()?;
494 if let Some((multi_index, mtime, flen)) = multi_index_info {
495 let index_names_in_multi_index: Vec<_> = multi_index.index_names().iter().map(AsRef::as_ref).collect();
496 let mut indices_not_in_multi_index: Vec<(Either, _, _)> = indices
497 .into_iter()
498 .filter_map(|(path, a, b)| {
499 (path != multi_index.path()
500 && !index_names_in_multi_index
501 .contains(&Path::new(path.file_name().expect("file name present"))))
502 .then_some((Either::IndexPath(path), a, b))
503 })
504 .collect();
505 indices_not_in_multi_index.insert(0, (Either::MultiIndexFile(Arc::new(multi_index)), mtime, flen));
506 indices_by_modification_time.extend(indices_not_in_multi_index);
507 } else {
508 indices_by_modification_time.extend(
509 indices
510 .into_iter()
511 .filter_map(|(p, a, b)| (!is_multipack_index(&p)).then_some((Either::IndexPath(p), a, b))),
512 );
513 }
514 }
515 // Unlike libgit2, do not sort by modification date, but by size and put the biggest indices first. That way
516 // the chance to hit an object should be higher. We leave it to the handle to sort by LRU.
517 // Git itself doesn't change the order which may save time, but we want it to be stable which also helps some tests.
518 // NOTE: this will work well for well-packed repos or those using geometric repacking, but force us to open a lot
519 // of files when dealing with new objects, as there is no notion of recency here as would be with unmaintained
520 // repositories. Different algorithms should be provided, like newest packs first, and possibly a mix of both
521 // with big packs first, then sorting by recency for smaller packs.
522 // We also want to implement `fetch.unpackLimit` to alleviate this issue a little.
523 indices_by_modification_time.sort_by(|l, r| l.2.cmp(&r.2).reverse());
524 Ok(indices_by_modification_time)
525 }
526
527 /// returns `Ok(dest_slot_was_empty)` if the copy could happen because dest-slot was actually free or disposable.
528 #[allow(clippy::too_many_arguments)]
529 fn try_set_index_slot(
530 lock: &parking_lot::MutexGuard<'_, ()>,
531 dest_slot: &MutableIndexAndPack,
532 index_info: Either,
533 mtime: SystemTime,
534 current_generation: Generation,
535 needs_stable_indices: bool,
536 ) -> Result<bool, Either> {
537 let (dest_slot_was_empty, generation) = match &**dest_slot.files.load() {
538 Some(bundle) => {
539 if bundle.index_path() == index_info.path() || (bundle.is_disposable() && needs_stable_indices) {
540 // it might be possible to see ourselves in case all slots are taken, but there are still a few more destination
541 // slots to look for.
542 return Err(index_info);
543 }
544 // Since we overwrite an existing slot, we have to increment the generation to prevent anyone from trying to use it while
545 // before we are replacing it with a different value.
546 // In detail:
547 // We need to declare this to be the future to avoid anything in that slot to be returned to people who
548 // last saw the old state. They will then try to get a new index which by that time, might be happening
549 // in time so they get the latest one. If not, they will probably get into the same situation again until
550 // it finally succeeds. Alternatively, the object will be reported unobtainable, but at least it won't return
551 // some other object.
552 (false, current_generation + 1)
553 }
554 None => {
555 // For multi-pack indices:
556 // Do NOT copy the packs over, they need to be reopened to get the correct pack id matching the new slot map index.
557 // If we are allowed to delete the original, and nobody has the pack referenced, it is closed which is preferred.
558 // Thus we simply always start new with packs in multi-pack indices.
559 // In the worst case this could mean duplicate file handle usage though as the old and the new index can't share
560 // packs due to the intrinsic id.
561 // 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.
562 (true, current_generation)
563 }
564 };
565 Self::set_slot_to_index(lock, dest_slot, index_info, mtime, generation);
566 Ok(dest_slot_was_empty)
567 }
568
569 fn set_slot_to_index(
570 _lock: &parking_lot::MutexGuard<'_, ()>,
571 slot: &MutableIndexAndPack,
572 index_info: Either,
573 mtime: SystemTime,
574 generation: Generation,
575 ) {
576 let _lock = slot.write.lock();
577 let mut files = slot.files.load_full();
578 let files_mut = Arc::make_mut(&mut files);
579 // set the generation before we actually change the value, otherwise readers of old generations could observe the new one.
580 // We rather want them to turn around here and update their index, which, by that time, might actually already be available.
581 // If not, they would fail unable to load a pack or index they need, but that's preferred over returning wrong objects.
582 // Safety: can't race as we hold the lock, have to set the generation beforehand to help avoid others to observe the value.
583 slot.generation.store(generation, Ordering::SeqCst);
584 *files_mut = Some(index_info.into_index_and_packs(mtime));
585 slot.files.store(files);
586 }
587
588 /// Returns true if the index was left in a loaded state.
589 fn assure_slot_matches_index(
590 _lock: &parking_lot::MutexGuard<'_, ()>,
591 slot: &MutableIndexAndPack,
592 index_info: Either,
593 mtime: SystemTime,
594 current_generation: Generation,
595 ) -> bool {
596 match Option::as_ref(&slot.files.load()) {
597 Some(bundle) => {
598 assert_eq!(
599 bundle.index_path(),
600 index_info.path(),
601 "Parallel writers cannot change the file the slot points to."
602 );
603 if bundle.is_disposable() {
604 // put it into the correct mode, it's now available for sure so should not be missing or garbage.
605 // The latter can happen if files are removed and put back for some reason, but we should definitely
606 // have them in a decent state now that we know/think they are there.
607 let _lock = slot.write.lock();
608 let mut files = slot.files.load_full();
609 let files_mut = Arc::make_mut(&mut files)
610 .as_mut()
611 .expect("BUG: cannot change from something to nothing, would be race");
612 files_mut.put_back();
613 debug_assert_eq!(
614 files_mut.mtime(),
615 mtime,
616 "BUG: we can only put back files that didn't obviously change"
617 );
618 // Safety: can't race as we hold the lock, must be set before replacing the data.
619 // NOTE that we don't change the generation as it's still the very same index we talk about, it doesn't change
620 // identity.
621 slot.generation.store(current_generation, Ordering::SeqCst);
622 slot.files.store(files);
623 } else {
624 // it's already in the correct state, either loaded or unloaded.
625 }
626 bundle.index_is_loaded()
627 }
628 None => {
629 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")
630 }
631 }
632 }
633
634 /// Stability means that indices returned by this API will remain valid.
635 /// Without that constraint, we may unload unused packs and indices, and may rebuild the slotmap index.
636 ///
637 /// Note that this must be called with a lock to the relevant state held to assure these values don't change while
638 /// we are working on said index.
639 fn maintain_stable_indices(&self, _guard: &parking_lot::MutexGuard<'_, ()>) -> bool {
640 self.num_handles_stable.load(Ordering::SeqCst) > 0
641 }
642
643 pub(crate) fn collect_snapshot(&self) -> Snapshot {
644 // We don't observe changes-on-disk in our 'wait-for-load' loop.
645 // That loop is meant to help assure the marker (which includes the amount of loaded indices) matches
646 // the actual amount of indices we collect.
647 let index = self.index.load();
648 loop {
649 if index.num_indices_currently_being_loaded.deref().load(Ordering::SeqCst) != 0 {
650 std::thread::yield_now();
651 continue;
652 }
653 let marker = index.marker();
654 let indices = if index.is_initialized() {
655 index
656 .slot_indices
657 .iter()
658 .map(|idx| (*idx, &self.files[*idx]))
659 .filter_map(|(id, file)| {
660 let lookup = match (**file.files.load()).as_ref()? {
661 types::IndexAndPacks::Index(bundle) => handle::SingleOrMultiIndex::Single {
662 index: bundle.index.loaded()?.clone(),
663 data: bundle.data.loaded().cloned(),
664 },
665 types::IndexAndPacks::MultiIndex(multi) => handle::SingleOrMultiIndex::Multi {
666 index: multi.multi_index.loaded()?.clone(),
667 data: multi.data.iter().map(|f| f.loaded().cloned()).collect(),
668 },
669 };
670 handle::IndexLookup { file: lookup, id }.into()
671 })
672 .collect()
673 } else {
674 Vec::new()
675 };
676
677 return Snapshot {
678 indices,
679 loose_dbs: Arc::clone(&index.loose_dbs),
680 marker,
681 };
682 }
683 }
684}
685
686// Outside of this method we will never assign new slot indices.
687fn is_multipack_index(path: &Path) -> bool {
688 path.file_name() == Some(OsStr::new("multi-pack-index"))
689}
690
691struct IncOnNewAndDecOnDrop<'a>(&'a AtomicU16);
692impl<'a> IncOnNewAndDecOnDrop<'a> {
693 pub fn new(v: &'a AtomicU16) -> Self {
694 v.fetch_add(1, Ordering::SeqCst);
695 Self(v)
696 }
697}
698impl Drop for IncOnNewAndDecOnDrop<'_> {
699 fn drop(&mut self) {
700 self.0.fetch_sub(1, Ordering::SeqCst);
701 }
702}
703
704pub(crate) enum Either {
705 IndexPath(PathBuf),
706 MultiIndexFile(Arc<gix_pack::multi_index::File>),
707}
708
709impl Either {
710 fn path(&self) -> &Path {
711 match self {
712 Either::IndexPath(p) => p,
713 Either::MultiIndexFile(f) => f.path(),
714 }
715 }
716
717 fn into_index_and_packs(self, mtime: SystemTime) -> IndexAndPacks {
718 match self {
719 Either::IndexPath(path) => IndexAndPacks::new_single(path, mtime),
720 Either::MultiIndexFile(file) => IndexAndPacks::new_multi_from_open_file(file, mtime),
721 }
722 }
723
724 fn is_multi_index(&self) -> bool {
725 matches!(self, Either::MultiIndexFile(_))
726 }
727}
728
729impl Eq for Either {}
730
731impl PartialEq<Self> for Either {
732 fn eq(&self, other: &Self) -> bool {
733 self.path().eq(other.path())
734 }
735}
736
737#[allow(clippy::non_canonical_partial_ord_impl)]
738impl PartialOrd<Self> for Either {
739 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
740 Some(self.path().cmp(other.path()))
741 }
742}
743
744impl Ord for Either {
745 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
746 self.path().cmp(other.path())
747 }
748}