From b20b5b7c9b559f137c29a60523c8c8a6efe91686 Mon Sep 17 00:00:00 2001 From: Nishad <133812901+nishu-builder@users.noreply.github.com> Date: Fri, 18 Sep 2026 10:50:30 +0000 Subject: [PATCH] Keep server object reads working as Git packs accumulate --- rust/crates/server/src/main.rs | 27 ++++++++++++++++++++++++++- rust/crates/server/src/storage.rs | 7 +++++-- tests/git-import/fixture.py | 14 ++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/rust/crates/server/src/main.rs b/rust/crates/server/src/main.rs index 08bcc1947..a9ae5d7e3 100644 --- a/rust/crates/server/src/main.rs +++ b/rust/crates/server/src/main.rs @@ -263,7 +263,32 @@ fn main() { // Open the object database once as a thread-safe handle; each request thread // takes a cheap local handle from it (see `handle`). - let repo = match gix::open(&git_dir) { + // Packed uploads accumulate while automatic repacking is disabled. gix + // fixes its slot capacity at open time; its client default (at least 32) + // cannot accommodate a running server. Reserve its supported maximum. + // This is headroom, not pack maintenance: 32,767 is still a hard limit. + // TODO: consolidate packs during coordinated downtime, preserving ALL CAS + // objects (including unreferenced ones), before this capacity is exhausted. + // Do not enable live gc/repack: see the shared-reader constraints above. + // Given skips gix's initial scan, so diagnose an already-full store here. + let packs = std::fs::read_dir(std::path::Path::new(&git_dir).join("objects/pack")) + .map(|entries| { + entries + .filter_map(Result::ok) + .filter(|entry| entry.path().extension().is_some_and(|ext| ext == "idx")) + .count() + }) + .unwrap_or(0); + if packs >= (1 << 15) - 1 { + eprintln!("fatal: object store has {packs} packs; consolidate packs offline, preserving unreferenced CAS objects, before restarting"); + std::process::exit(1); + } + if packs >= 30_000 { + eprintln!("warning: object store has {packs} packs, approaching the 32767-pack limit; schedule offline consolidation"); + } + let options = gix::open::Options::default() + .object_store_slots(gix::odb::store::init::Slots::Given((1 << 15) - 1)); + let repo = match gix::open_opts(&git_dir, options) { Ok(repo) => repo.into_sync(), Err(err) => { eprintln!("fatal: cannot open git repo at {git_dir}: {err}"); diff --git a/rust/crates/server/src/storage.rs b/rust/crates/server/src/storage.rs index 208ed77c8..308cdfcab 100644 --- a/rust/crates/server/src/storage.rs +++ b/rust/crates/server/src/storage.rs @@ -123,14 +123,17 @@ fn validate_commit_with_git(config: &Config, content: &[u8]) -> Result<(), HttpE format!("creating commit validation repository: {error}"), ) })?; + // Stage the candidate before attaching the live store. Only Git needs + // the alternate for validation; this one-object gix handle must not try + // to load all of the server's packs into its startup-sized slot table. + let id = gix::objs::Write::write_buf(&repo.objects, gix::object::Kind::Commit, content) + .map_err(|error| HttpError::new(500, format!("staging commit: {error}")))?; let objects = std::fs::canonicalize(std::path::Path::new(&config.git_dir).join("objects"))?; std::fs::create_dir_all(staged.0.join("objects/info"))?; std::fs::write( staged.0.join("objects/info/alternates"), format!("{}\n", objects.display()), )?; - let id = gix::objs::Write::write_buf(&repo.objects, gix::object::Kind::Commit, content) - .map_err(|error| HttpError::new(500, format!("staging commit: {error}")))?; let git = || { let mut command = std::process::Command::new("git"); command.env_clear(); diff --git a/tests/git-import/fixture.py b/tests/git-import/fixture.py index f8dc988ff..ae4f7598b 100644 --- a/tests/git-import/fixture.py +++ b/tests/git-import/fixture.py @@ -460,6 +460,20 @@ def visible(commit): invalid = subprocess.run([cli, "import-git", *args], env=cli_env, capture_output=True) assert invalid.returncode != 0 assert token.encode() not in invalid.stdout + invalid.stderr + # Small uploads now stay packed too. The live reader must discover + # more packs than gix's default capacity of 32 without a restart. + for index in range(40): + packed = advance() + run("git", "--git-dir", str(origin), "push", "-q", base, + packed[0] + f":refs/heads/pack-growth-{index}") + for oid in packed: + object_request(oid) + assert len(list((odb / "objects/pack").glob("*.pack"))) > 32 + # Commit staging must also work once its live-store alternate + # contains more than 32 packs. + staged_tip = advance(packed[0]) + for kind, oid in [("blob", staged_tip[2]), ("tree", staged_tip[1]), ("commit", staged_tip[0])]: + post_from_origin(kind, oid) stop(server); server = None server = start() visible(posted_child[0])