~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

A mirror's repack runs aside, not in the way of its reads

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:serve/tidy-aside
crates/knotless/src/mirror.rs+59−2
@@ -38,6 +38,19 @@ impl Lock {
}
}
}
/// The lock at `path` if no one holds it, without waiting.
pub(crate) fn try_acquire(path: PathBuf) -> Result<Option<Lock>> {
let file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.open(&path)?;
match file.try_lock() {
Err(std::fs::TryLockError::WouldBlock) => Ok(None),
done => Ok(Some(done.map(|()| Lock { _file: file })?)),
}
}
}
pub struct Mirror {
@@ -85,8 +98,27 @@ impl Mirror {
}
/// Fetches what `snap` names and sets the mirror's refs and HEAD to match
/// it exactly. Processes sharing the cache take turns at the refs.
/// it exactly, then [tidies](Mirror::tidy) it.
pub async fn sync(&self, remote: &Remote, snap: &Snapshot) -> Result<()> {
self.refresh(remote, snap).await?;
self.tidy().await
}
/// Makes the mirror's packs one once there are more than [`PACKS`],
/// unless another process is at it already. Reads and syncs go on
/// meanwhile: a repack of a large mirror takes seconds, so one that
/// answers reads runs it aside rather than in their way.
pub async fn tidy(&self) -> Result<()> {
let lock = Lock::try_acquire(self.git.dir.with_extension("tidy"))?;
if lock.is_some() {
self.git.tidy(PACKS).await?;
}
Ok(())
}
/// [`Mirror::sync`] without tidying, for one that tidies aside. Processes
/// sharing the cache take turns at the refs.
pub async fn refresh(&self, remote: &Remote, snap: &Snapshot) -> Result<()> {
let Some(key) = snap.key() else {
return Ok(());
};
@@ -111,7 +143,6 @@ impl Mirror {
}
self.git.update_refs(&script).await?;
}
self.git.tidy(PACKS).await?;
if moved || !self.git.has_graph() {
self.git.graph().await?;
}
@@ -142,6 +173,26 @@ impl Mirror {
mod tests {
use super::*;
#[tokio::test]
async fn a_tidy_under_way_is_left_to_finish() {
let dir = tempfile::tempdir().unwrap();
let mirror = Mirror {
git: Git::init_bare(&dir.path().join("m.git")).await.unwrap(),
};
mirror.tidy().await.unwrap();
// Another holds the tidy's lock: this one does nothing, and waits
// for nothing.
let held = Lock::try_acquire(mirror.git.dir.with_extension("tidy")).unwrap();
assert!(held.is_some());
mirror.tidy().await.unwrap();
drop(held);
// A mirror with nowhere for the lock is not tidied.
let gone = Mirror {
git: Git::new(dir.path().join("no/such.git")),
};
assert!(gone.tidy().await.is_err());
}
#[tokio::test]
async fn locks() {
let dir = tempfile::tempdir().unwrap();
@@ -165,6 +216,12 @@ mod tests {
assert!(!waiter.is_finished());
drop(held);
drop(waiter.await.unwrap().unwrap());
// Taken without waiting: there only when no one holds it.
let held = Lock::try_acquire(path.clone()).unwrap().unwrap();
assert!(Lock::try_acquire(path.clone()).unwrap().is_none());
drop(held);
assert!(Lock::try_acquire(path.clone()).unwrap().is_some());
assert!(Lock::try_acquire(dir.path().join("no/such/dir.lock")).is_err());
assert!(
Lock::acquire(dir.path().join("no/such/dir.lock"))
.await
crates/knotless-serve/src/gateway.rs+2−1
@@ -554,9 +554,10 @@ async fn mirror(app: &App, remote: &Remote, snap: &Snapshot) -> Result<Mirror, A
.await
.map_err(AppError::internal)?;
mirror
.sync(remote, snap)
.refresh(remote, snap)
.await
.map_err(AppError::upstream)?;
crate::aside(&mirror);
Ok(mirror)
}
crates/knotless-serve/src/lib.rs+11−1
@@ -584,9 +584,10 @@ impl App {
.await
.map_err(AppError::internal)?;
mirror
.sync(&remote, &snap)
.refresh(&remote, &snap)
.await
.map_err(AppError::upstream)?;
aside(&mirror);
entry.found = Some((remote.did().to_string(), key.to_string(), repo.rkey.clone()));
entry.record = repo.value.clone();
entry.space = remote.space.as_ref().map(|s| s.uri.clone());
@@ -615,6 +616,15 @@ impl App {
}
}
/// Tidies `mirror` in a task of its own, out of the way of the request
/// that synced it and of every other waiting on that sync.
pub(crate) fn aside(mirror: &Mirror) {
let mirror = Mirror {
git: mirror.git.clone(),
};
tokio::spawn(async move { mirror.tidy().await.ok() });
}
/// Indexes a write the event carried in full, made in the commit at `rev`
/// when it says: a deletion, or a record delivered inline. `false` when
/// the record has to be fetched.
docs/design/storage.md+5−1
@@ -436,7 +436,11 @@ renamed into place, so racing processes end up with the same one.
A mirror is created with git's own gc off (`gc.auto=0`). Instead, a sync
that leaves it holding more than 50 packs repacks them into one, keeping
objects no ref reaches.
objects no ref reaches. The repack runs outside the lock on the refs, under
a lock of its own that a second repack skips rather than waits for. A large
mirror's repack takes seconds. knotless-serve starts it in a task of its own
once a sync has set the refs, so the request that synced, and every request
waiting on that sync, goes on reading the packs that are there meanwhile.
A sync that moves the refs, or finds a mirror with no commit-graph, then
writes one (`git commit-graph write --reachable --split`): each commit's