~ 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 refs are set holding their lock until git is done

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:serve/mirror-lock-held
crates/knotless/src/mirror.rs+40−37
@@ -126,47 +126,50 @@ impl Mirror {
let wants: Vec<String> = live.iter().map(|(_, sha)| sha.clone()).collect();
// Indexing packs needs no lock: each lands under its own name.
fetch::fetch(remote, key, &self.git, &wants, Some(snap.packs.clone())).await?;
let _lock = Lock::acquire(self.git.dir.with_extension("lock")).await?;
let mut want = live;
want.sort();
let local = self.git.refs().await?;
let moved = local != want;
if moved {
let mut script = String::new();
for (name, sha) in &want {
script.push_str(&format!("update {name} {sha}\n"));
}
for (name, _) in &local {
if !want.iter().any(|(n, _)| n == name) {
script.push_str(&format!("delete {name}\n"));
}
}
self.git.update_refs(&script).await?;
}
if moved || !self.git.has_graph() {
self.git.graph().await?;
let branch = snap.repo.as_ref();
let branch = branch.and_then(|r| r.value.default_branch.clone());
// Set in a task of its own, which holds the lock until git is done:
// a caller that stops waiting, as a request whose asker left, drops
// what it awaits, and the lock with it while git still holds the
// refs, and the next sync's `update-ref` then fails on git's own
// lock file.
let settled = tokio::spawn(settle(self.git.clone(), live, branch));
settled.await?
}
}
/// Sets `git`'s refs to `live` exactly, and its HEAD to `branch`, under the
/// lock processes sharing the cache take turns at the refs by.
async fn settle(git: Git, live: Vec<(String, String)>, branch: Option<String>) -> Result<()> {
let _lock = Lock::acquire(git.dir.with_extension("lock")).await?;
let mut want = live;
want.sort();
let local = git.refs().await?;
let moved = local != want;
if moved {
let mut script = String::new();
for (name, sha) in &want {
script.push_str(&format!("update {name} {sha}\n"));
}
if let Some(branch) = snap
.repo
.as_ref()
.and_then(|r| r.value.default_branch.as_deref())
{
let head = format!("ref: refs/heads/{branch}\n");
if std::fs::read_to_string(self.git.dir.join("HEAD"))
.ok()
.as_deref()
!= Some(head.as_str())
{
self.git
.run(
&["symbolic-ref", "HEAD", &format!("refs/heads/{branch}")],
None,
)
.await?;
for (name, _) in &local {
if !want.iter().any(|(n, _)| n == name) {
script.push_str(&format!("delete {name}\n"));
}
}
Ok(())
git.update_refs(&script).await?;
}
if moved || !git.has_graph() {
git.graph().await?;
}
if let Some(branch) = branch {
let head = format!("ref: refs/heads/{branch}\n");
let now = std::fs::read_to_string(git.dir.join("HEAD")).ok();
if now.as_deref() != Some(head.as_str()) {
let to = format!("refs/heads/{branch}");
git.run(&["symbolic-ref", "HEAD", &to], None).await?;
}
}
Ok(())
}
#[cfg(test)]
docs/design/storage.md+4−1
@@ -431,7 +431,10 @@ exactly to the snapshot's in one `git update-ref --stdin` transaction. Packs
are indexed without a lock, because each lands under its own name. The ref
transaction runs under the operating system's lock on a file beside the
mirror. The system lets go of it when its process ends, so a crashed process
cannot block the mirror. A new mirror is created under a temporary name and
cannot block the mirror. The transaction runs in a task of its own that
holds the lock until git is done. A caller that stops waiting, such as a
request whose asker left, does not end it, so the next sync never finds
git's own lock files on the refs still there. A new mirror is created under a temporary name and
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