From cf08a3f7315143a98dd2f6aa75e09c1035554f22 Mon Sep 17 00:00:00 2001 From: Fintan Halpenny Date: Tue, 31 Oct 2023 15:19:34 +0000 Subject: [PATCH] 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= 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 X-Clacks-Overhead: GNU Terry Pratchett --- radicle-node/src/runtime.rs | 1 + radicle-node/src/worker.rs | 14 +++++++- radicle-node/src/worker/garbage.rs | 53 ++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 radicle-node/src/worker/garbage.rs diff --git a/radicle-node/src/runtime.rs b/radicle-node/src/runtime.rs index d7a4b056..05c30d21 100644 --- a/radicle-node/src/runtime.rs +++ b/radicle-node/src/runtime.rs @@ -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, diff --git a/radicle-node/src/worker.rs b/radicle-node/src/worker.rs index 36528269..148400c3 100644 --- a/radicle-node/src/worker.rs +++ b/radicle-node/src/worker.rs @@ -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) } } diff --git a/radicle-node/src/worker/garbage.rs b/radicle-node/src/worker/garbage.rs new file mode 100644 index 00000000..ec19b14f --- /dev/null +++ b/radicle-node/src/worker/garbage.rs @@ -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 { + 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) +}