node: run git gc on fetch

It is desirable to keep the Git repositories in Radicle efficient by
ensuring objects are packed and unreachable objects are removed.

Add a process call to `git gc` to achieve this. Two flags are used for
garbage collection: `--auto` and `--prune`. Their details are outlined below:

       --auto
           With this option, git gc checks whether any housekeeping is
           required; if not, it exits without performing any work.

       --prune=<date>
           Prune loose objects older than date (default is 2 weeks
           ago, overridable by the config variable
           gc.pruneExpire). --prune=now prunes loose objects
           regardless of their age and increases the risk of
           corruption if another process is writing to the repository
           concurrently; see "NOTES" below. --prune is on by default.

For `--prune` the default used is `1.hours.ago`. This was chosen over
`now` since that may be too aggressive after just fetching new data.

Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
X-Clacks-Overhead: GNU Terry Pratchett
This commit is contained in:
Fintan Halpenny 2023-10-31 15:19:34 +00:00 committed by cloudhead
parent dd16356e4e
commit cf08a3f731
No known key found for this signature in database
3 changed files with 67 additions and 1 deletions

View File

@ -256,6 +256,7 @@ impl Runtime {
limit: FetchLimit::default(),
info: git::UserInfo { alias, key: nid },
local: nid,
expiry: worker::garbage::Expiry::default(),
};
let pool = worker::Pool::with(
worker_recv,

View File

@ -3,6 +3,7 @@ mod channels;
mod upload_pack;
pub mod fetch;
pub mod garbage;
use std::path::PathBuf;
use std::{io, time};
@ -162,6 +163,9 @@ pub struct FetchConfig {
pub info: git::UserInfo,
/// Public key of the local peer.
pub local: crypto::PublicKey,
/// Configuration for `git gc` garbage collection. Defaults to `1
/// hour ago`.
pub expiry: garbage::Expiry,
}
/// A worker that replicates git objects.
@ -279,6 +283,7 @@ impl Worker {
limit,
info,
local,
expiry,
} = &self.fetch_config;
let tracking =
tracking::Config::new(*policy, *scope, tracking::Store::reader(tracking_db)?);
@ -296,8 +301,15 @@ impl Worker {
blocked,
channels,
)?;
let result = handle.fetch(rid, &self.storage, *limit, remote, refs_at)?;
Ok(handle.fetch(rid, &self.storage, *limit, remote, refs_at)?)
if let Err(e) = garbage::collect(&self.storage, rid, *expiry) {
// N.b. ensure that `git gc` works in debug mode.
debug_assert!(false, "`git gc` failed: {e}");
log::warn!(target: "worker", "Failed to run `git gc`: {e}");
}
Ok(result)
}
}

View File

@ -0,0 +1,53 @@
use std::process::{Command, ExitStatus, Stdio};
use std::{fmt, io};
use radicle::prelude::Id;
use radicle::storage::ReadStorage;
/// Default expiry time for objects.
pub const EXPIRY_DEFAULT: Expiry = Expiry::Hours(1);
/// Expiry of objects for garbage collector.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Expiry {
Now,
Seconds(usize),
Hours(usize),
Days(usize),
Weeks(usize),
}
impl Default for Expiry {
fn default() -> Self {
EXPIRY_DEFAULT
}
}
impl fmt::Display for Expiry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Now => f.write_str("now"),
Self::Seconds(s) => write!(f, "{s}.seconds.ago"),
Self::Hours(s) => write!(f, "{s}.hours.ago"),
Self::Days(s) => write!(f, "{s}.days.ago"),
Self::Weeks(s) => write!(f, "{s}.weeks.ago"),
}
}
}
/// Run Git garbage collector.
pub fn collect(storage: &impl ReadStorage, rid: Id, expiry: Expiry) -> io::Result<ExitStatus> {
let git_dir = storage.path_of(&rid);
let mut gc = Command::new("git");
gc.current_dir(git_dir)
.env_clear()
.envs(std::env::vars().filter(|(key, _)| key == "PATH" || key.starts_with("GIT_TRACE")))
.args(["gc", &format!("--prune={expiry}"), "--auto"])
.stdout(Stdio::piped())
.stdin(Stdio::piped())
.stderr(Stdio::inherit());
let mut child = gc.spawn()?;
let status = child.wait()?;
Ok(status)
}