fetch: remove `.keep` files
A `.keep` file in the `objects/pack` directory acts as a lock to ensure that operations like `git gc` do not clean up `.idx` files while a fetch is being performed. It is safe to remove the `.keep` file once a fetch has completed and a reference points to a given object in the packfile. To ensure this happens in `radicle-fetch`, a newtype `Keepfile` is introduced. It uses special `Drop` semantics by attempting to remove the file when the object is dropped -- logging a warning if it fails to do so. `FetchState` is augmented to keep a set of the `Keepfile`s. This ensures that when the fetch completes, either successfully or with a failure, when `FetchState` is dropped then so are the `Keepfile`s. We ensure that the expected behaviour of removing the `.keep`files occurs by adding a check in `e2e::test_replication` to check Alice's repository's packs. Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com> X-Clacks-Overhead: GNU Terry Pratchett
This commit is contained in:
parent
8402b85ad9
commit
d56d619f56
|
|
@ -1,5 +1,6 @@
|
||||||
pub(crate) mod mem;
|
pub(crate) mod mem;
|
||||||
pub(crate) mod repository;
|
pub(crate) mod repository;
|
||||||
|
pub(crate) mod packfile;
|
||||||
|
|
||||||
pub mod refs;
|
pub mod refs;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// The [`PathBuf`] which points to a `*.keep` file, which should correspond to
|
||||||
|
/// a packfile.
|
||||||
|
///
|
||||||
|
/// Upon drop, it attempts to remove the [`PathBuf`] to release the lock on the
|
||||||
|
/// packfile index, allowing it to be garbage collected.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Keepfile {
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keepfile {
|
||||||
|
pub fn new<P: AsRef<Path>>(path: P) -> Option<Self> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
match path.extension() {
|
||||||
|
Some(ext) if ext == "keep" => Some(Self {
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Keepfile {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Err(e) = fs::remove_file(&self.path) {
|
||||||
|
log::warn!(target: "fetch", "Failed to remove {:?}: {e}", self.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ use radicle::storage::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::git;
|
use crate::git;
|
||||||
|
use crate::git::packfile::Keepfile;
|
||||||
use crate::git::refs::{Applied, Update};
|
use crate::git::refs::{Applied, Update};
|
||||||
use crate::git::repository;
|
use crate::git::repository;
|
||||||
use crate::sigrefs::SignedRefsAt;
|
use crate::sigrefs::SignedRefsAt;
|
||||||
|
|
@ -160,6 +161,10 @@ pub struct FetchState {
|
||||||
sigrefs: SigrefTips,
|
sigrefs: SigrefTips,
|
||||||
/// Seen reference tips, per remote.
|
/// Seen reference tips, per remote.
|
||||||
tips: BTreeMap<PublicKey, Vec<Update<'static>>>,
|
tips: BTreeMap<PublicKey, Vec<Update<'static>>>,
|
||||||
|
/// The `.keep` files created during packfile transfers. They are kept
|
||||||
|
/// within the state, so that when the state is dropped, it also attempts to
|
||||||
|
/// delete the files to release the locks on the packfiles.
|
||||||
|
keepfiles: Vec<Keepfile>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FetchState {
|
impl FetchState {
|
||||||
|
|
@ -233,9 +238,11 @@ impl FetchState {
|
||||||
|
|
||||||
let wants_haves = step.wants_haves(&handle.repo, &refs)?;
|
let wants_haves = step.wants_haves(&handle.repo, &refs)?;
|
||||||
if !wants_haves.wants.is_empty() {
|
if !wants_haves.wants.is_empty() {
|
||||||
handle
|
let keepfile =
|
||||||
.transport
|
handle
|
||||||
.fetch(wants_haves, handle.interrupt.clone(), handshake)?;
|
.transport
|
||||||
|
.fetch(wants_haves, handle.interrupt.clone(), handshake)?;
|
||||||
|
self.keepfiles.extend(keepfile);
|
||||||
} else {
|
} else {
|
||||||
log::trace!(target: "fetch", "Nothing to fetch")
|
log::trace!(target: "fetch", "Nothing to fetch")
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ use radicle::storage::git::Repository;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::git::oid;
|
use crate::git::oid;
|
||||||
|
use crate::git::packfile::Keepfile;
|
||||||
use crate::git::repository;
|
use crate::git::repository;
|
||||||
|
|
||||||
/// Open a reader and writer stream to pass to the ls-refs and fetch
|
/// Open a reader and writer stream to pass to the ls-refs and fetch
|
||||||
|
|
@ -120,7 +121,7 @@ where
|
||||||
wants_haves: WantsHaves,
|
wants_haves: WantsHaves,
|
||||||
interrupt: Arc<AtomicBool>,
|
interrupt: Arc<AtomicBool>,
|
||||||
handshake: &handshake::Outcome,
|
handshake: &handshake::Outcome,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<Option<Keepfile>> {
|
||||||
log::trace!(
|
log::trace!(
|
||||||
target: "fetch",
|
target: "fetch",
|
||||||
"Running fetch wants={:?}, haves={:?}",
|
"Running fetch wants={:?}, haves={:?}",
|
||||||
|
|
@ -170,7 +171,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(out.keepfile)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Signal to the server side that we are done sending ls-refs and
|
/// Signal to the server side that we are done sending ls-refs and
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ use gix_transport::client;
|
||||||
use gix_transport::client::{ExtendedBufRead, MessageKind};
|
use gix_transport::client::{ExtendedBufRead, MessageKind};
|
||||||
use gix_transport::Protocol;
|
use gix_transport::Protocol;
|
||||||
|
|
||||||
|
use crate::git::packfile;
|
||||||
|
|
||||||
use super::{agent_name, indicate_end_of_interaction, Connection, WantsHaves};
|
use super::{agent_name, indicate_end_of_interaction, Connection, WantsHaves};
|
||||||
|
|
||||||
pub type Error = gix_protocol::fetch::Error;
|
pub type Error = gix_protocol::fetch::Error;
|
||||||
|
|
@ -102,6 +104,7 @@ pub struct Fetch {
|
||||||
pub struct FetchOut {
|
pub struct FetchOut {
|
||||||
pub refs: Vec<Ref>,
|
pub refs: Vec<Ref>,
|
||||||
pub pack: Option<pack::bundle::write::Outcome>,
|
pub pack: Option<pack::bundle::write::Outcome>,
|
||||||
|
pub keepfile: Option<packfile::Keepfile>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: the delegate pattern will be removed in the near future and
|
// FIXME: the delegate pattern will be removed in the near future and
|
||||||
|
|
@ -127,6 +130,7 @@ impl<'a> Delegate for &'a mut Fetch {
|
||||||
.pack_writer
|
.pack_writer
|
||||||
.write_pack(input, progress)
|
.write_pack(input, progress)
|
||||||
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
|
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
|
||||||
|
self.out.keepfile = pack.keep_path.as_ref().and_then(packfile::Keepfile::new);
|
||||||
self.out.pack = Some(pack);
|
self.out.pack = Some(pack);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -204,6 +208,7 @@ where
|
||||||
out: FetchOut {
|
out: FetchOut {
|
||||||
refs: Vec::new(),
|
refs: Vec::new(),
|
||||||
pack: None,
|
pack: None,
|
||||||
|
keepfile: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,21 @@ fn test_replication() {
|
||||||
alice.storage.repository(acme).unwrap().validate(),
|
alice.storage.repository(acme).unwrap().validate(),
|
||||||
Ok(validations) if validations.is_empty()
|
Ok(validations) if validations.is_empty()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Ensure that .keep files are deleted upon replication
|
||||||
|
{
|
||||||
|
let repo = alice.storage.repository(acme).unwrap();
|
||||||
|
let pack_dir = repo.path().join("objects").join("pack");
|
||||||
|
for entry in std::fs::read_dir(pack_dir).unwrap() {
|
||||||
|
let entry = entry.unwrap();
|
||||||
|
let path = entry.path();
|
||||||
|
assert_ne!(
|
||||||
|
path.extension(),
|
||||||
|
Some("keep".as_ref()),
|
||||||
|
"found .keep file after fetch: {path:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue