chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
A mirror's refs are set holding their lock until git is done
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 | ||