chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
A mirror's repack runs aside, not in the way of its reads
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 | ||