Skip to content

Commit e4fb21e

Browse files
authored
Merge pull request #1750 from GitoxideLabs/odb-issue
handle many packs better
2 parents 8df5ba2 + dbf079f commit e4fb21e

File tree

6 files changed

+90
-4
lines changed

6 files changed

+90
-4
lines changed

gix-odb/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ pub struct Store {
119119

120120
/// The below state acts like a slot-map with each slot is mutable when the write lock is held, but readable independently of it.
121121
/// This allows multiple file to be loaded concurrently if there is multiple handles requesting to load packs or additional indices.
122-
/// The map is static and cannot typically change.
122+
/// The map is static and cannot change.
123123
/// It's read often and changed rarely.
124124
pub(crate) files: Vec<types::MutableIndexAndPack>,
125125

gix-odb/src/store_impls/dynamic/load_index.rs

+9-3
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ impl super::Store {
266266
Option::as_ref(&files_guard).expect("slot is set or we wouldn't know it points to this file");
267267
if index_info.is_multi_index() && files.mtime() != mtime {
268268
// 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,
269+
// that are currently available. Instead, we have to move what's there into a new slot, along with the changes,
270270
// and later free the slot or dispose of the index in the slot (like we do for removed/missing files).
271271
index_paths_to_add.push_back((index_info, mtime, Some(slot_idx)));
272272
// If the current slot is loaded, the soon-to-be copied multi-index path will be loaded as well.
@@ -304,6 +304,12 @@ impl super::Store {
304304
needed: index_paths_to_add.len() + 1, /*the one currently popped off*/
305305
});
306306
}
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+
}
307313
let slot_index = next_possibly_free_index;
308314
let slot = &self.files[slot_index];
309315
next_possibly_free_index = (next_possibly_free_index + 1) % self.files.len();
@@ -502,7 +508,7 @@ impl super::Store {
502508
}
503509
// Unlike libgit2, do not sort by modification date, but by size and put the biggest indices first. That way
504510
// the chance to hit an object should be higher. We leave it to the handle to sort by LRU.
505-
// Git itself doesn't change the order which may safe time, but we want it to be stable which also helps some tests.
511+
// Git itself doesn't change the order which may save time, but we want it to be stable which also helps some tests.
506512
// NOTE: this will work well for well-packed repos or those using geometric repacking, but force us to open a lot
507513
// of files when dealing with new objects, as there is no notion of recency here as would be with unmaintained
508514
// repositories. Different algorithms should be provided, like newest packs first, and possibly a mix of both
@@ -512,7 +518,7 @@ impl super::Store {
512518
Ok(indices_by_modification_time)
513519
}
514520

515-
/// returns Ok<dest slot was empty> if the copy could happen because dest-slot was actually free or disposable , and Some(true) if it was empty
521+
/// returns `Ok(dest_slot_was_empty)` if the copy could happen because dest-slot was actually free or disposable.
516522
#[allow(clippy::too_many_arguments)]
517523
fn try_set_index_slot(
518524
lock: &parking_lot::MutexGuard<'_, ()>,

gix/src/commit.rs

+8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
//!
22
#![allow(clippy::empty_docs)]
33

4+
use std::convert::Infallible;
5+
46
/// An empty array of a type usable with the `gix::easy` API to help declaring no parents should be used
57
pub const NO_PARENT_IDS: [gix_hash::ObjectId; 0] = [];
68

@@ -22,6 +24,12 @@ pub enum Error {
2224
ReferenceEdit(#[from] crate::reference::edit::Error),
2325
}
2426

27+
impl From<std::convert::Infallible> for Error {
28+
fn from(_value: Infallible) -> Self {
29+
unreachable!("cannot be invoked")
30+
}
31+
}
32+
2533
///
2634
#[cfg(feature = "revision")]
2735
pub mod describe {

gix/src/remote/connection/fetch/update_refs/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ pub(crate) fn update(
102102
let update = if is_implicit_tag {
103103
Mode::ImplicitTagNotSentByRemote.into()
104104
} else {
105+
// Assure the ODB is not to blame for the missing object.
106+
repo.try_find_object(remote_id)?;
105107
Mode::RejectedSourceObjectNotFound { id: remote_id.into() }.into()
106108
};
107109
updates.push(update);

gix/src/remote/connection/fetch/update_refs/update.rs

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ mod error {
2323
PeelToId(#[from] crate::reference::peel::Error),
2424
#[error("Failed to follow a symbolic reference to assure worktree isn't affected")]
2525
FollowSymref(#[from] gix_ref::file::find::existing::Error),
26+
#[error(transparent)]
27+
FindObject(#[from] crate::object::find::Error),
2628
}
2729
}
2830

gix/tests/gix/remote/fetch.rs

+68
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,74 @@ mod blocking_and_async_io {
8585
try_repo_rw(name).unwrap()
8686
}
8787

88+
#[test]
89+
#[cfg(feature = "blocking-network-client")]
90+
fn fetch_more_packs_than_can_be_handled() -> gix_testtools::Result {
91+
use gix::config::tree::User;
92+
use gix::interrupt::IS_INTERRUPTED;
93+
use gix_odb::store::init::Slots;
94+
use gix_testtools::tempfile;
95+
fn create_empty_commit(repo: &gix::Repository) -> anyhow::Result<()> {
96+
let name = repo.head_name()?.expect("no detached head");
97+
repo.commit(
98+
name.as_bstr(),
99+
"empty",
100+
gix::hash::ObjectId::empty_tree(repo.object_hash()),
101+
repo.try_find_reference(name.as_ref())?.map(|r| r.id()),
102+
)?;
103+
Ok(())
104+
}
105+
for max_packs in 1..=3 {
106+
let remote_dir = tempfile::tempdir()?;
107+
let mut remote_repo = gix::init_bare(remote_dir.path())?;
108+
{
109+
let mut config = remote_repo.config_snapshot_mut();
110+
config.set_value(&User::NAME, "author")?;
111+
config.set_value(&User::EMAIL, "[email protected]")?;
112+
}
113+
create_empty_commit(&remote_repo)?;
114+
115+
let local_dir = tempfile::tempdir()?;
116+
let (local_repo, _) = gix::clone::PrepareFetch::new(
117+
remote_repo.path(),
118+
local_dir.path(),
119+
gix::create::Kind::Bare,
120+
Default::default(),
121+
gix::open::Options::isolated().object_store_slots(Slots::Given(max_packs)),
122+
)?
123+
.fetch_only(gix::progress::Discard, &IS_INTERRUPTED)?;
124+
125+
let remote = local_repo
126+
.branch_remote(
127+
local_repo.head_ref()?.expect("branch available").name().shorten(),
128+
Fetch,
129+
)
130+
.expect("remote is configured after clone")?;
131+
for _round_to_create_pack in 1..12 {
132+
create_empty_commit(&remote_repo)?;
133+
match remote
134+
.connect(Fetch)?
135+
.prepare_fetch(gix::progress::Discard, Default::default())?
136+
.receive(gix::progress::Discard, &IS_INTERRUPTED)
137+
{
138+
Ok(out) => {
139+
for local_tracking_branch_name in out.ref_map.mappings.into_iter().filter_map(|m| m.local) {
140+
let r = local_repo.find_reference(&local_tracking_branch_name)?;
141+
r.id()
142+
.object()
143+
.expect("object should be present after fetching, triggering pack refreshes works");
144+
local_repo.head_ref()?.unwrap().set_target_id(r.id(), "post fetch")?;
145+
}
146+
}
147+
Err(err) => assert!(err
148+
.to_string()
149+
.starts_with("The slotmap turned out to be too small with ")),
150+
}
151+
}
152+
}
153+
Ok(())
154+
}
155+
88156
#[test]
89157
#[cfg(feature = "blocking-network-client")]
90158
#[allow(clippy::result_large_err)]

0 commit comments

Comments
 (0)