use std::collections::HashSet;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use alkblobs::Key;
use async_trait::async_trait;
use gix_hash::ObjectId;
use gix_object::Kind;
use alkgit::StorageError;
use crate::odb::oid_key;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RefRow {
pub repo_id: String,
pub name: String,
pub state: RefRowState,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RefRowState {
Oid(ObjectId),
Symref(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RefRowsSnapshot {
pub rows: Vec<RefRow>,
pub fingerprint: u64,
}
#[async_trait]
pub trait RefRows: Send + Sync {
async fn snapshot(&self) -> Result<RefRowsSnapshot, StorageError>;
}
pub(crate) fn fingerprint_rows(rows: &[RefRow]) -> u64 {
fn fold(state: &mut u64, part: &[u8]) {
for b in part {
*state ^= u64::from(*b);
*state = state.wrapping_mul(0x0100_0000_0193_e739);
}
*state ^= 0x0f4e_a559_f1bf_2acd;
*state = state.wrapping_mul(0x0100_0000_0193_e739);
}
let mut sorted: Vec<&RefRow> = rows.iter().collect();
sorted.sort_by(|a, b| (&a.repo_id, &a.name).cmp(&(&b.repo_id, &b.name)));
let mut state = 0xcbf2_9ce4_8422_2325u64;
fold(&mut state, &(sorted.len() as u64).to_le_bytes());
for row in sorted {
fold(&mut state, row.repo_id.as_bytes());
fold(&mut state, row.name.as_bytes());
match &row.state {
RefRowState::Oid(oid) => {
fold(&mut state, &[0x01]);
fold(&mut state, oid.as_bytes());
}
RefRowState::Symref(target) => {
fold(&mut state, &[0x02]);
fold(&mut state, target.as_bytes());
}
}
}
state
}
struct Memo {
fingerprint: u64,
build_writes: u64,
keys: HashSet<Key>,
}
pub struct RefClosure {
front: crate::odb::ObjectFront,
rows: Arc<dyn RefRows>,
writes: Arc<AtomicU64>,
memo: parking_lot::Mutex<Option<Memo>>,
builds: AtomicU64,
}
impl RefClosure {
pub fn new(
front: crate::odb::ObjectFront,
rows: Arc<dyn RefRows>,
writes: Arc<AtomicU64>,
) -> Arc<Self> {
Arc::new(RefClosure {
front,
rows,
writes,
memo: parking_lot::Mutex::new(None),
builds: AtomicU64::new(0),
})
}
pub fn register(
self: &Arc<Self>,
store: &alkblobs::Store,
) -> Result<alkblobs::Registration, alkblobs::Error> {
store.register_liveness_source(alkblobs::LivenessRegistration::InProcess(
self.clone() as Arc<dyn alkblobs::LivenessSource>
))
}
pub fn keys_builds(&self) -> u64 {
self.builds.load(Ordering::Relaxed)
}
async fn rebuild_or_cached_keys(&self) -> Result<Vec<Key>, StorageError> {
let build_writes = self.writes.load(Ordering::Relaxed);
let snapshot = self.rows.snapshot().await?;
{
let mut memo = self.memo.lock();
if let Some(memo) = memo.as_mut() {
if memo.fingerprint == snapshot.fingerprint {
memo.build_writes = build_writes;
return Ok(memo.keys.iter().cloned().collect());
}
}
}
let (keys, fingerprint) = self.walk(&snapshot).await;
self.builds.fetch_add(1, Ordering::Relaxed);
*(self.memo.lock()) = Some(Memo {
fingerprint,
build_writes,
keys: keys.iter().cloned().collect(),
});
Ok(keys)
}
async fn walk(&self, snapshot: &RefRowsSnapshot) -> (Vec<Key>, u64) {
let fingerprint = snapshot.fingerprint;
let mut tips: Vec<ObjectId> = snapshot
.rows
.iter()
.filter_map(|row| match &row.state {
RefRowState::Oid(oid) => Some(*oid),
RefRowState::Symref(_) => None,
})
.collect();
tips.sort();
tips.dedup();
let mut visited: HashSet<ObjectId> = HashSet::new();
let mut keys: Vec<Key> = Vec::new();
let mut stack = tips;
while let Some(oid) = stack.pop() {
if !visited.insert(oid) {
continue;
}
let Ok(key) = oid_key(&oid) else {
continue;
};
let Some((kind, body)) = self.front.get(&oid).await.ok().flatten() else {
continue;
};
keys.push(key);
let body = body.as_slice();
match kind {
Kind::Blob => {}
Kind::Commit => {
let Ok(commit) = gix_object::CommitRef::from_bytes(body, gix_hash::Kind::Sha1)
else {
continue;
};
for parent in std::iter::once(&commit.tree).chain(commit.parents.iter()) {
if let Ok(id) = ObjectId::from_hex(parent) {
stack.push(id);
}
}
}
Kind::Tree => {
for entry in gix_object::TreeRefIter::from_bytes(body, gix_hash::Kind::Sha1) {
let Ok(entry) = entry else { continue };
match entry.mode.kind() {
gix_object::tree::EntryKind::Tree
| gix_object::tree::EntryKind::Blob
| gix_object::tree::EntryKind::BlobExecutable
| gix_object::tree::EntryKind::Link => {
stack.push(entry.oid.to_owned());
}
gix_object::tree::EntryKind::Commit => {}
}
}
}
Kind::Tag => {
let Ok(tag) = gix_object::TagRef::from_bytes(body, gix_hash::Kind::Sha1) else {
continue;
};
if let Ok(target) = ObjectId::from_hex(tag.target) {
stack.push(target);
}
}
}
}
(keys, fingerprint)
}
}
impl alkblobs::LivenessSource for RefClosure {
fn live_keys(&self) -> Vec<Key> {
self.front
.block_in_place(self.rebuild_or_cached_keys())
.unwrap_or_default()
}
fn contains(&self, key: &Key) -> bool {
let memo = self.memo.lock();
let Some(memo) = memo.as_ref() else {
return true;
};
if memo.keys.contains(key) {
return true;
}
let writes_now = self.writes.load(Ordering::Relaxed);
writes_now != memo.build_writes
}
}
#[cfg(feature = "lfs")]
use crate::lfs::LfsRows;
#[cfg(feature = "lfs")]
pub fn lfs_media_key(oid_hex: &str) -> Result<Key, StorageError> {
let hex_ok = oid_hex.len() == 64
&& oid_hex
.bytes()
.all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'));
if !hex_ok {
return Err(StorageError::Invalid(format!(
"the LFS media oid is not a 64-lowercase-hex sha256 form: {oid_hex:?}"
)));
}
let mut bytes = Vec::with_capacity(1 + 32);
bytes.push(alkblobs::Algorithm::GitObjectSha256.byte());
let pair_value = |pair: &[u8]| {
let hi = (pair[0] as char).to_digit(16).ok_or_else(|| {
StorageError::Invalid("the media-family digest half is not hex".to_owned())
})?;
let lo = (pair[1] as char).to_digit(16).ok_or_else(|| {
StorageError::Invalid("the media-family digest half is not hex".to_owned())
})?;
Ok((hi * 16 + lo) as u8)
};
for pair in oid_hex.as_bytes().chunks(2) {
bytes.push(pair_value(pair)?);
}
Key::from_bytes(&bytes)
.map_err(|e| StorageError::Invalid(format!("the media-family key does not decode: {e}")))
}
#[cfg(feature = "lfs")]
struct MediaMemo {
build_writes: u64,
keys: HashSet<Key>,
}
#[cfg(feature = "lfs")]
pub struct LfsMedia {
rows: Arc<dyn LfsRows>,
writes: Arc<AtomicU64>,
runtime: tokio::runtime::Handle,
memo: parking_lot::Mutex<Option<MediaMemo>>,
builds: AtomicU64,
}
#[cfg(feature = "lfs")]
impl LfsMedia {
pub fn new(rows: Arc<dyn LfsRows>, writes: Arc<AtomicU64>) -> Arc<Self> {
Arc::new(LfsMedia {
rows,
writes,
runtime: tokio::runtime::Handle::current(),
memo: parking_lot::Mutex::new(None),
builds: AtomicU64::new(0),
})
}
pub fn register(
self: &Arc<Self>,
store: &alkblobs::Store,
) -> Result<alkblobs::Registration, alkblobs::Error> {
store.register_liveness_source(alkblobs::LivenessRegistration::InProcess(
self.clone() as Arc<dyn alkblobs::LivenessSource>
))
}
pub fn keys_builds(&self) -> u64 {
self.builds.load(Ordering::Relaxed)
}
fn block_in_place<F>(&self, fut: F) -> F::Output
where
F: std::future::Future + Send,
F::Output: Send,
{
tokio::task::block_in_place(move || self.runtime.block_on(fut))
}
async fn rebuild_keys(&self) -> Result<Vec<Key>, StorageError> {
let build_writes = self.writes.load(Ordering::Relaxed);
let rows = self.rows.rows().await?;
let mut keys: HashSet<Key> = HashSet::new();
for row in &rows {
if let Ok(key) = lfs_media_key(&row.oid) {
keys.insert(key);
}
}
*(self.memo.lock()) = Some(MediaMemo {
build_writes,
keys: keys.clone(),
});
self.builds.fetch_add(1, Ordering::Relaxed);
let mut out: Vec<Key> = keys.into_iter().collect();
out.sort();
Ok(out)
}
}
#[cfg(feature = "lfs")]
impl alkblobs::LivenessSource for LfsMedia {
fn live_keys(&self) -> Vec<Key> {
self.block_in_place(self.rebuild_keys()).unwrap_or_default()
}
fn contains(&self, key: &Key) -> bool {
let memo = self.memo.lock();
let Some(memo) = memo.as_ref() else {
return true;
};
if memo.keys.contains(key) {
return true;
}
self.writes.load(Ordering::Relaxed) != memo.build_writes
}
}
#[cfg(test)]
mod tests {
use alkblobs::Store;
use alkgit::{GitRefs, RefUpdate};
use gix_hash::ObjectId;
use gix_object::Kind;
use std::sync::atomic::AtomicU64;
use std::sync::Arc;
use crate::odb::{key_to_oid, ObjectFront};
use crate::refs::InMemoryRefTable;
use crate::testutil::private_record as repo;
#[cfg(feature = "sqlite")]
use crate::testutil::scratch_dir;
use super::*;
fn writes() -> Arc<AtomicU64> {
Arc::new(AtomicU64::new(0))
}
fn mem_fixture() -> (ObjectFront, Arc<InMemoryRefTable>, Arc<AtomicU64>) {
let front = ObjectFront::new(Arc::new(Store::mem_only()));
(front, Arc::new(InMemoryRefTable::new()), writes())
}
async fn put_object(front: &ObjectFront, kind: Kind, content: &[u8]) -> ObjectId {
let (key, _pin) = front.put(kind, content).await.unwrap();
key_to_oid(&key).expect("the put's key is the family key")
}
async fn create_ref(table: &dyn GitRefs, id: &str, name: &str, oid: ObjectId) {
let outcomes = table
.apply_updates(
&repo(id),
vec![RefUpdate {
name: name.to_owned(),
expected: None,
new: Some(oid),
}],
false,
)
.await
.unwrap();
assert!(outcomes.iter().all(|o| o.result.is_ok()));
}
fn blob_content() -> &'static [u8] {
b"hello\n"
}
enum TreeEntry<'a> {
Blob(&'a str, &'a ObjectId),
Link(&'a str, &'a ObjectId),
Gitlink(&'a str, &'a ObjectId),
}
fn tree_bytes(entries: &[TreeEntry<'_>]) -> Vec<u8> {
let mode_of = |e: &TreeEntry<'_>| match e {
TreeEntry::Blob(..) => "100644 ",
TreeEntry::Link(..) => "120000 ",
TreeEntry::Gitlink(..) => "160000 ",
};
let mut tree = Vec::new();
let mut sorted: Vec<(&str, &str, &ObjectId)> = entries
.iter()
.map(|e| match e {
TreeEntry::Blob(n, o) | TreeEntry::Link(n, o) | TreeEntry::Gitlink(n, o) => {
(mode_of(e), *n, *o)
}
})
.collect();
sorted.sort();
for (mode, name, oid) in sorted {
tree.extend_from_slice(mode.as_bytes());
tree.extend_from_slice(name.as_bytes());
tree.push(0);
tree.extend_from_slice(oid.as_bytes());
}
tree
}
fn commit_bytes(tree: &ObjectId, parents: &[&ObjectId]) -> Vec<u8> {
let mut body = format!("tree {tree}\n");
for parent in parents {
body.push_str(&format!("parent {parent}\n"));
}
body.push_str(
"author A <a@example.invalid> 1750000000 +0000\n\
committer A <a@example.invalid> 1750000000 +0000\n\n",
);
body.into_bytes()
}
fn tag_bytes(target: &ObjectId, target_kind: Kind) -> Vec<u8> {
format!(
"object {target}\n\
type {target_kind}\n\
tag v1\n\
tagger A <a@example.invalid> 1750000000 +0000\n\nmessage\n"
)
.into_bytes()
}
async fn fixture_graph(front: &ObjectFront) -> GraphFixture {
let blob = put_object(front, Kind::Blob, blob_content()).await;
let blob2 = put_object(front, Kind::Blob, b"other\n").await;
let tree = front
.put(Kind::Tree, &tree_bytes(&[TreeEntry::Blob("hello", &blob)]))
.await
.map(|(key, _)| key_to_oid(&key).unwrap())
.unwrap();
let gitlink_subtree_oid = front
.put(Kind::Tree, &tree_bytes(&[TreeEntry::Blob("g", &blob)]))
.await
.map(|(key, _)| key_to_oid(&key).unwrap())
.unwrap();
let gitlink_target = put_object(
front,
Kind::Commit,
&commit_bytes(&gitlink_subtree_oid, &[]),
)
.await;
let gitlink_tree = front
.put(
Kind::Tree,
&tree_bytes(&[
TreeEntry::Blob("other", &blob2),
TreeEntry::Link("sym", &blob2),
TreeEntry::Gitlink("sub", &gitlink_target),
]),
)
.await
.map(|(key, _)| key_to_oid(&key).unwrap())
.unwrap();
let base = front
.put(Kind::Commit, &commit_bytes(&tree, &[]))
.await
.map(|(key, _)| key_to_oid(&key).unwrap())
.unwrap();
let tip = front
.put(Kind::Commit, &commit_bytes(&tree, &[&base]))
.await
.map(|(key, _)| key_to_oid(&key).unwrap())
.unwrap();
let tag = front
.put(Kind::Tag, &tag_bytes(&tip, Kind::Commit))
.await
.map(|(key, _)| key_to_oid(&key).unwrap())
.unwrap();
GraphFixture {
blob,
blob2,
tree,
base,
tip,
tag,
gitlink_target,
gitlink_tree,
}
}
#[derive(Clone, Copy)]
struct GraphFixture {
blob: ObjectId,
blob2: ObjectId,
tree: ObjectId,
base: ObjectId,
tip: ObjectId,
tag: ObjectId,
gitlink_target: ObjectId,
gitlink_tree: ObjectId,
}
async fn covered_fixture(front: &ObjectFront) -> GraphFixture {
let blob = put_object(front, Kind::Blob, blob_content()).await;
let tree = front
.put(Kind::Tree, &tree_bytes(&[TreeEntry::Blob("hello", &blob)]))
.await
.map(|(key, _)| key_to_oid(&key).unwrap())
.unwrap();
let base = front
.put(Kind::Commit, &commit_bytes(&tree, &[]))
.await
.map(|(key, _)| key_to_oid(&key).unwrap())
.unwrap();
let tip = front
.put(Kind::Commit, &commit_bytes(&tree, &[&base]))
.await
.map(|(key, _)| key_to_oid(&key).unwrap())
.unwrap();
let oid = ObjectId::from([0u8; 20]);
GraphFixture {
blob,
blob2: oid,
tree,
base,
tip,
tag: oid,
gitlink_target: oid,
gitlink_tree: oid,
}
}
async fn live_oids(source: &Arc<RefClosure>) -> Vec<ObjectId> {
let mut oids: Vec<ObjectId> = alkblobs::LivenessSource::live_keys(source.as_ref())
.iter()
.filter_map(key_to_oid)
.collect();
oids.sort();
oids
}
fn sorted(mut oids: Vec<ObjectId>) -> Vec<ObjectId> {
oids.sort();
oids
}
#[tokio::test(flavor = "multi_thread")]
async fn the_closure_walk_covers_the_full_graph() {
let (front, refs, writes) = mem_fixture();
let fix = fixture_graph(&front).await;
create_ref(refs.as_ref(), "r/one", "refs/heads/main", fix.tip).await;
create_ref(refs.as_ref(), "r/one", "refs/tags/v1", fix.tag).await;
create_ref(refs.as_ref(), "r/two", "refs/heads/wip", fix.gitlink_tree).await;
let orphan = put_object(&front, Kind::Blob, b"orphan\n").await;
let seeded_rows = Arc::new(SymrefSeededRows::new(refs.clone()));
seeded_rows.seed_head();
let rows: Arc<dyn RefRows> = seeded_rows;
let source = RefClosure::new(front.clone(), rows, writes);
assert_eq!(source.keys_builds(), 0, "construction walks nothing");
let oids = live_oids(&source).await;
let mut expected = sorted(vec![
fix.blob,
fix.blob2,
fix.tree,
fix.base,
fix.tip,
fix.tag,
fix.gitlink_tree,
]);
expected.dedup();
assert_eq!(
oids, expected,
"the closure covers the commit ancestry (parents included), the trees and blobs, and the peeled tag"
);
assert!(
!oids.contains(&fix.gitlink_target),
"a gitlink entry neither marks nor walks (the other repo's refs cover it)"
);
assert!(!oids.contains(&orphan), "an unreachable object is not live");
assert_eq!(source.keys_builds(), 1);
for key in alkblobs::LivenessSource::live_keys(source.as_ref()) {
assert_eq!(key.as_bytes()[0], 0x03, "the sha1 family byte");
}
}
struct SymrefSeededRows {
inner: Arc<InMemoryRefTable>,
head: parking_lot::Mutex<Option<RefRow>>,
}
impl SymrefSeededRows {
fn new(inner: Arc<InMemoryRefTable>) -> Self {
SymrefSeededRows {
inner,
head: parking_lot::Mutex::new(None),
}
}
fn seed_head(self: &Arc<Self>) {
*(self.head.lock()) = Some(RefRow {
repo_id: "r/one".to_owned(),
name: "HEAD".to_owned(),
state: RefRowState::Symref("refs/heads/main".to_owned()),
});
}
}
#[async_trait]
impl RefRows for SymrefSeededRows {
async fn snapshot(&self) -> Result<RefRowsSnapshot, StorageError> {
let mut rows = self.inner.snapshot().await?.rows;
if let Some(head) = self.head.lock().clone() {
rows.push(head);
}
rows.sort_by(|a, b| (&a.repo_id, &a.name).cmp(&(&b.repo_id, &b.name)));
Ok(RefRowsSnapshot {
fingerprint: fingerprint_rows(&rows),
rows,
})
}
}
#[tokio::test(flavor = "multi_thread")]
async fn the_sweep_reclaims_orphans_when_the_source_is_registered() {
let (front, refs, writes) = mem_fixture();
let fix = covered_fixture(&front).await;
create_ref(refs.as_ref(), "repo", "refs/heads/main", fix.tip).await;
let rows: Arc<dyn RefRows> = refs.clone();
let orphan_one = put_object(&front, Kind::Blob, b"orphan one\n").await;
let orphan_two = put_object(&front, Kind::Blob, b"orphan two\n").await;
let store = front.store().clone();
let source = RefClosure::new(front.clone(), rows, writes);
source.register(&store).unwrap();
let report = store.sweep().await.unwrap();
assert!(
report.aborted.is_none(),
"a registered source never trips NoLivenessSources"
);
assert_eq!(
report.deleted, 2,
"both orphans reclaimed (the pool: the covered closure + the two orphans)"
);
assert!(front.stat(&orphan_one).await.unwrap().is_none(), "gone");
assert!(front.stat(&orphan_two).await.unwrap().is_none(), "gone");
for live in [fix.tip, fix.base, fix.tree, fix.blob] {
assert!(
front.stat(&live).await.unwrap().is_some(),
"the closure stays"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn a_ref_delete_uncovers_its_closure_for_the_sweep() {
let (front, refs, writes) = mem_fixture();
let fix = fixture_graph(&front).await;
create_ref(refs.as_ref(), "repo", "refs/heads/main", fix.tip).await;
let rows: Arc<dyn RefRows> = refs.clone();
let store = front.store().clone();
let source = RefClosure::new(front.clone(), rows, writes);
source.register(&store).unwrap();
let first = alkblobs::LivenessSource::live_keys(source.as_ref()).len();
assert_eq!(first, 4, "tip, base, tree, blob — the covered closure");
for live in [fix.tip, fix.base, fix.tree, fix.blob] {
assert!(front.stat(&live).await.unwrap().is_some());
}
let outcomes = refs
.apply_updates(
&repo("repo"),
vec![RefUpdate {
name: "refs/heads/main".to_owned(),
expected: Some(fix.tip),
new: None,
}],
false,
)
.await
.unwrap();
assert!(outcomes.iter().all(|o| o.result.is_ok()));
let after = alkblobs::LivenessSource::live_keys(source.as_ref());
assert_eq!(
after.len(),
0,
"the delete-uncovered closure leaves the live set after the rebuild"
);
let report = store.sweep().await.unwrap();
assert!(report.aborted.is_none());
for gone in [fix.tip, fix.base, fix.tree, fix.blob] {
assert!(front.stat(&gone).await.unwrap().is_none(), "reclaimed");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn the_memo_is_keyed_on_the_rows_fingerprint() {
let (front, refs, writes) = mem_fixture();
let fix = fixture_graph(&front).await;
create_ref(refs.as_ref(), "repo", "refs/heads/main", fix.tip).await;
let rows: Arc<dyn RefRows> = refs.clone();
let source = RefClosure::new(front.clone(), rows, writes);
alkblobs::LivenessSource::live_keys(source.as_ref());
assert_eq!(source.keys_builds(), 1, "the first consult walks");
alkblobs::LivenessSource::live_keys(source.as_ref());
assert_eq!(
source.keys_builds(),
1,
"an unchanged fingerprint consumes no walk"
);
source.writes.fetch_add(1, Ordering::Relaxed);
let memoized = alkblobs::LivenessSource::live_keys(source.as_ref()).len();
assert_eq!(memoized, 4, "the memo answers");
assert_eq!(
source.keys_builds(),
1,
"a counter bump alone does not rebuild"
);
create_ref(refs.as_ref(), "repo", "refs/heads/topic", fix.base).await;
alkblobs::LivenessSource::live_keys(source.as_ref());
assert_eq!(source.keys_builds(), 2, "a fingerprint move rebuilds");
let oids = live_oids(&source).await;
assert_eq!(oids, sorted(vec![fix.tip, fix.base, fix.tree, fix.blob]));
}
#[tokio::test(flavor = "multi_thread")]
async fn the_contains_consult_never_derives() {
let (front, refs, writes) = mem_fixture();
let fix = fixture_graph(&front).await;
create_ref(refs.as_ref(), "repo", "refs/heads/main", fix.tip).await;
let rows: Arc<dyn RefRows> = refs.clone();
let source = RefClosure::new(front.clone(), rows, writes);
let tip_key = oid_key(&fix.tip).unwrap();
let absent_key = oid_key(&ObjectId::from([1u8; 20])).unwrap();
assert!(
alkblobs::LivenessSource::contains(source.as_ref(), &tip_key),
"a cold memo answers live — the safe default"
);
assert_eq!(source.keys_builds(), 0, "contains never derives");
assert!(alkblobs::LivenessSource::contains(
source.as_ref(),
&absent_key
));
alkblobs::LivenessSource::live_keys(source.as_ref());
assert_eq!(source.keys_builds(), 1);
assert!(alkblobs::LivenessSource::contains(
source.as_ref(),
&tip_key
));
assert!(
!alkblobs::LivenessSource::contains(source.as_ref(), &absent_key),
"a built memo answers the index exactly"
);
source.writes.fetch_add(1, Ordering::Relaxed);
assert!(
alkblobs::LivenessSource::contains(source.as_ref(), &absent_key),
"a moved write counter answers conservatively (unknown is live)"
);
assert_eq!(
source.keys_builds(),
1,
"the arbitration consult never triggers the walk"
);
alkblobs::LivenessSource::live_keys(source.as_ref());
assert!(!alkblobs::LivenessSource::contains(
source.as_ref(),
&absent_key
));
assert!(alkblobs::LivenessSource::contains(
source.as_ref(),
&tip_key
));
}
#[tokio::test(flavor = "multi_thread")]
async fn the_walk_degrades_best_effort_on_missing_nodes() {
let (front, refs, writes) = mem_fixture();
let blob = put_object(&front, Kind::Blob, blob_content()).await;
let tree = front
.put(Kind::Tree, &tree_bytes(&[TreeEntry::Blob("hello", &blob)]))
.await
.map(|(key, _)| key_to_oid(&key).unwrap())
.unwrap();
let missing_parent = ObjectId::from([9u8; 20]);
let tip = front
.put(Kind::Commit, &commit_bytes(&tree, &[&missing_parent]))
.await
.map(|(key, _)| key_to_oid(&key).unwrap())
.unwrap();
let dangling = ObjectId::from([5u8; 20]);
create_ref(refs.as_ref(), "repo", "refs/heads/main", tip).await;
create_ref(refs.as_ref(), "repo", "refs/heads/dangling", dangling).await;
let rows: Arc<dyn RefRows> = refs.clone();
let source = RefClosure::new(front.clone(), rows, writes);
let oids = live_oids(&source).await;
assert_eq!(oids, sorted(vec![tip, tree, blob]));
}
#[tokio::test]
#[should_panic]
async fn the_live_verb_panics_on_a_current_thread_runtime() {
let (front, refs, writes) = mem_fixture();
let tip = put_object(&front, Kind::Blob, b"tip\n").await;
create_ref(refs.as_ref(), "repo", "refs/heads/main", tip).await;
let rows: Arc<dyn RefRows> = refs.clone();
let source = RefClosure::new(front, rows, writes);
alkblobs::LivenessSource::live_keys(source.as_ref());
}
#[test]
fn the_fingerprint_is_deterministic_and_sensitive() {
let row = |name: &str, oid: u8| RefRow {
repo_id: "repo".to_owned(),
name: name.to_owned(),
state: RefRowState::Oid(ObjectId::from([oid; 20])),
};
let rows = vec![row("a", 1), row("b", 2)];
let again = vec![row("a", 1), row("b", 2)];
assert_eq!(fingerprint_rows(&rows), fingerprint_rows(&again));
let moved_oid = vec![row("a", 1), row("b", 3)];
let moved_name = vec![row("a", 1), row("c", 2)];
let reordered = vec![row("b", 2), row("a", 1)];
assert_ne!(fingerprint_rows(&rows), fingerprint_rows(&moved_oid));
assert_ne!(fingerprint_rows(&rows), fingerprint_rows(&moved_name));
assert_eq!(
fingerprint_rows(&rows),
fingerprint_rows(&reordered),
"the canonical fingerprint is order-normalized"
);
let symref = RefRow {
repo_id: "repo".to_owned(),
name: "HEAD".to_owned(),
state: RefRowState::Symref("refs/heads/main".to_owned()),
};
let with_symref = vec![row("a", 1), symref];
assert_ne!(fingerprint_rows(&rows), fingerprint_rows(&with_symref));
assert_ne!(fingerprint_rows(&[]), fingerprint_rows(&[row("a", 1)]));
}
#[cfg(feature = "sqlite")]
#[tokio::test(flavor = "multi_thread")]
async fn the_sqlite_rows_seam_snapshots_canonically() {
use crate::migrations::CoTenantSqlite;
use crate::refs::SqliteRefTable;
let dir = scratch_dir("liveness", "rows");
let handle = CoTenantSqlite::open(dir.join("rows.sqlite3")).unwrap();
let table = Arc::new(SqliteRefTable::new(handle));
let tip = ObjectId::from([4u8; 20]);
create_ref(table.as_ref(), "repo", "refs/heads/main", tip).await;
create_ref(table.as_ref(), "other", "refs/heads/wip", tip).await;
let rows: Arc<dyn RefRows> = table.clone();
let first = rows.snapshot().await.unwrap();
assert_eq!(first.rows.len(), 2);
assert_eq!(first.rows[0].repo_id, "other", "repo id sorts first");
assert_eq!(first.rows[1].repo_id, "repo");
assert_eq!(first.fingerprint, fingerprint_rows(&first.rows));
let second = rows.snapshot().await.unwrap();
assert_eq!(
first.fingerprint, second.fingerprint,
"an unchanged table fingerprints equal"
);
create_ref(table.as_ref(), "repo", "refs/heads/topic", tip).await;
let third = rows.snapshot().await.unwrap();
assert_ne!(third.fingerprint, second.fingerprint);
std::fs::remove_dir_all(&dir).unwrap_or_default();
}
#[cfg(feature = "sqlite")]
#[tokio::test(flavor = "multi_thread")]
async fn the_versioned_wrapper_forwards_and_bumps() {
use crate::migrations::CoTenantSqlite;
use crate::refs::{SqliteRefTable, VersionedRefTable};
let dir = scratch_dir("liveness", "wrapper");
let handle = CoTenantSqlite::open(dir.join("wrapper.sqlite3")).unwrap();
let inner = Arc::new(SqliteRefTable::new(handle));
let counter = Arc::new(AtomicU64::new(0));
let wrapper = Arc::new(VersionedRefTable::new(inner.clone(), counter.clone()));
assert_eq!(counter.load(Ordering::Relaxed), 0, "listing does not bump");
let tip = ObjectId::from([4u8; 20]);
wrapper
.apply_updates(
&repo("repo"),
vec![RefUpdate {
name: "refs/heads/main".to_owned(),
expected: None,
new: Some(tip),
}],
false,
)
.await
.unwrap();
assert_eq!(
counter.load(Ordering::Relaxed),
1,
"an applied CAS call bumps the counter"
);
let rows: Arc<dyn RefRows> = wrapper.clone();
assert_eq!(
rows.snapshot().await.unwrap().rows.len(),
1,
"the snapshot forwards through the wrapper"
);
let denied = wrapper
.apply_updates(
&repo("repo"),
vec![RefUpdate {
name: "refs/heads/main".to_owned(),
expected: Some(ObjectId::from([9u8; 20])),
new: Some(tip),
}],
false,
)
.await
.unwrap();
assert!(denied.iter().all(|o| o.result.is_err()));
assert_eq!(
counter.load(Ordering::Relaxed),
2,
"a green call bumps (the bump follows the committed rows; the denials ride the outcomes)"
);
std::fs::remove_dir_all(&dir).unwrap_or_default();
}
#[cfg(feature = "sqlite")]
#[tokio::test(flavor = "multi_thread")]
async fn the_composition_wiring_serves_the_sweep_end_to_end() {
use std::time::Duration;
use alkblobs::StoreMode;
use alkgit::GitRegistryStore as _;
use crate::sqlite::SqliteRegistryAssembly;
let dir = scratch_dir("liveness", "wiring");
let db = dir.join("composition.sqlite3");
let assembly = SqliteRegistryAssembly::open(&db).await.unwrap();
let record = repo("owner/wired");
let store = Arc::new(
Store::kv_only_sqlite(&db, 128 * 1024, Duration::ZERO)
.await
.unwrap(),
);
assert_eq!(store.mode(), StoreMode::KvOnly);
let front = ObjectFront::new(store.clone());
let rows: Arc<dyn RefRows> = assembly.refs().clone();
let source = RefClosure::new(front.clone(), rows, assembly.refs_version().clone());
source.register(&store).unwrap();
assembly.backend().put_repo(&record).await.unwrap();
let covered = put_object(&front, Kind::Blob, b"covered\n").await;
let orphan = put_object(&front, Kind::Blob, b"orphan\n").await;
assert!(
front.get(&covered).await.unwrap().is_some()
&& front.get(&orphan).await.unwrap().is_some(),
"both land in the pool"
);
let first = store.sweep().await.unwrap();
assert!(first.aborted.is_none());
assert_eq!(first.candidates_staged, 2, "both orphans stage");
assert_eq!(first.deleted, 0);
assert!(
front.get(&orphan).await.unwrap().is_some(),
"staged, not deleted"
);
assert!(front.get(&covered).await.unwrap().is_some());
assembly
.refs()
.apply_updates(
&record,
vec![RefUpdate {
name: "refs/heads/main".to_owned(),
expected: None,
new: Some(covered),
}],
false,
)
.await
.unwrap();
let second = store.sweep().await.unwrap();
assert!(second.aborted.is_none());
assert_eq!(second.deleted, 0);
assert_eq!(
second.cancelled_by_arbitration, 2,
"the push-path bump cancels the staged candidates"
);
assert_eq!(second.candidates_staged, 1);
assert!(front.get(&orphan).await.unwrap().is_some());
assert!(front.get(&covered).await.unwrap().is_some());
let third = store.sweep().await.unwrap();
assert!(third.aborted.is_none());
assert_eq!(
third.deleted, 1,
"the orphan reclaims once the rebuild caught up"
);
assert!(front.get(&orphan).await.unwrap().is_none());
assert!(front.get(&covered).await.unwrap().is_some());
assembly
.refs()
.apply_updates(
&record,
vec![RefUpdate {
name: "refs/heads/main".to_owned(),
expected: Some(covered),
new: None,
}],
false,
)
.await
.unwrap();
let fourth = store.sweep().await.unwrap();
assert!(fourth.aborted.is_none());
assert_eq!(fourth.candidates_staged, 1, "the uncovered closure stages");
assert_eq!(fourth.deleted, 0);
assert!(
front.get(&covered).await.unwrap().is_some(),
"still in the window"
);
let fifth = store.sweep().await.unwrap();
assert_eq!(
fifth.deleted, 1,
"the overdue uncovered closure reclaims (the epoch already caught up at the delete-time mark)"
);
assert!(front.get(&covered).await.unwrap().is_none());
std::fs::remove_dir_all(&dir).unwrap_or_default();
}
}
#[cfg(all(test, feature = "lfs"))]
mod media_tests {
use std::sync::atomic::AtomicU64;
use std::sync::Arc;
use alkblobs::{Algorithm, Store};
use alkgit::{GitRefs, RefUpdate, RepoRecord};
use gix_hash::ObjectId;
use gix_object::Kind;
use std::io::Cursor;
use crate::lfs::InMemoryLfsRows;
use crate::liveness::{lfs_media_key, oid_key, LfsMedia};
use crate::odb::{key_to_oid, ObjectFront};
use crate::refs::InMemoryRefTable;
use crate::testutil::private_record as repo;
#[cfg(feature = "sqlite")]
use crate::testutil::scratch_dir;
use super::*;
async fn put_object(front: &ObjectFront, kind: Kind, content: &[u8]) -> ObjectId {
let (key, _pin) = front.put(kind, content).await.unwrap();
key_to_oid(&key).expect("the put's key is the family key")
}
fn writes() -> Arc<AtomicU64> {
Arc::new(AtomicU64::new(0))
}
fn media_fixture() -> (Arc<Store>, Arc<dyn crate::lfs::LfsRows>, Arc<AtomicU64>) {
let store = Arc::new(Store::mem_only());
let counter = writes();
let rows: Arc<dyn crate::lfs::LfsRows> = Arc::new(crate::lfs::VersionedLfsRows::new(
Arc::new(InMemoryLfsRows::new()),
Arc::clone(&counter),
));
(store, rows, counter)
}
async fn put_media(store: &Store, content: &[u8]) -> (String, Key) {
let (key, _pin) = store
.put_as(
Algorithm::GitObjectSha256,
Some(content.len() as u64),
Cursor::new(content),
)
.await
.unwrap();
let hex: String = key
.digest()
.bytes()
.iter()
.map(|b| format!("{b:02x}"))
.collect();
(hex, key)
}
fn contains_of(source: &Arc<LfsMedia>, key: &Key) -> bool {
alkblobs::LivenessSource::contains(source.as_ref(), key)
}
#[tokio::test(flavor = "multi_thread")]
async fn the_media_keys_form_map_free_and_reject_the_rest() {
let store = Store::mem_only();
let content = b"lfs media bytes\n";
let (hex, put_key) = put_media(&store, content).await;
assert_eq!(hex.len(), 64, "the digest's hex is the row oid");
assert_eq!(put_key.as_bytes()[0], 0x04, "the media family byte");
let key = lfs_media_key(&hex).unwrap();
assert_eq!(
key, put_key,
"the oid IS the digest — the key forms map-free"
);
assert_eq!(key.algorithm(), Algorithm::GitObjectSha256);
assert_eq!(
key.as_bytes(),
put_key.as_bytes(),
"the formed key is the physical encoding byte-exact"
);
let bad_forms: Vec<String> = vec![
"".to_owned(),
"nothex".to_owned(),
"AB".repeat(32),
"1".repeat(63),
"1".repeat(65),
"z".repeat(64),
];
for bad in bad_forms {
assert!(
matches!(lfs_media_key(&bad), Err(StorageError::Invalid(_))),
"a non-sha256-hex oid rejects: {bad:?}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn the_media_source_marks_the_rows_and_answers_the_arbitration_from_the_index() {
let (store, rows, counter) = media_fixture();
let source = LfsMedia::new(rows.clone(), Arc::clone(&counter));
let (_, key_absent) = put_media(&store, b"unasserted\n").await;
assert!(
contains_of(&source, &key_absent),
"a cold source answers live — the safe default"
);
assert_eq!(source.keys_builds(), 0, "contains never derives");
let (oid_a, key_a) = put_media(&store, b"shared\n").await;
let (oid_b, key_b) = put_media(&store, b"other\n").await;
rows.assert("r/one", &oid_a, 10).await.unwrap();
rows.assert("r/two", &oid_a, 10).await.unwrap();
rows.assert("r/one", &oid_b, 6).await.unwrap();
let keys = alkblobs::LivenessSource::live_keys(source.as_ref());
assert_eq!(
keys.len(),
2,
"one key with two rows is one live key (the dedup)"
);
assert!(keys.contains(&key_a) && keys.contains(&key_b));
assert_eq!(source.keys_builds(), 1);
alkblobs::LivenessSource::live_keys(source.as_ref());
assert_eq!(
source.keys_builds(),
2,
"rebuild-per-mark: a consult re-reads the rows even unchanged"
);
assert!(contains_of(&source, &key_a), "the index answer is exact");
assert!(
!contains_of(&source, &key_absent),
"an unasserted oid is exactly absent while the epoch is caught up"
);
let (oid_c, key_c) = put_media(&store, b"landed-late\n").await;
rows.assert("r/one", &oid_c, 12).await.unwrap();
assert!(contains_of(&source, &key_c), "unknown is live");
let keys = alkblobs::LivenessSource::live_keys(source.as_ref());
assert_eq!(keys.len(), 3, "the next mark's rebuild answers exactly");
assert!(contains_of(&source, &key_c), "now in the index");
}
struct GatedAssertLfsRows {
inner: Arc<crate::lfs::InMemoryLfsRows>,
gate: std::sync::Mutex<Option<tokio::sync::oneshot::Receiver<()>>>,
arrived: tokio::sync::watch::Sender<bool>,
}
#[async_trait::async_trait]
impl crate::lfs::LfsRows for GatedAssertLfsRows {
async fn assert(&self, repo_id: &str, oid: &str, size: u64) -> Result<(), StorageError> {
let rx = self.gate.lock().unwrap_or_else(|e| e.into_inner()).take();
if let Some(rx) = rx {
let _ = self.arrived.send(true);
let _ = rx.await;
}
self.inner.assert(repo_id, oid, size).await
}
async fn remove(&self, repo_id: &str, oid: &str) -> Result<(), StorageError> {
self.inner.remove(repo_id, oid).await
}
async fn remove_repo(&self, repo_id: &str) -> Result<(), StorageError> {
self.inner.remove_repo(repo_id).await
}
async fn rows(&self) -> Result<Vec<crate::lfs::LfsRow>, StorageError> {
self.inner.rows().await
}
async fn repo_rows(&self, repo_id: &str) -> Result<Vec<crate::lfs::LfsRow>, StorageError> {
self.inner.repo_rows(repo_id).await
}
}
#[tokio::test(flavor = "multi_thread")]
async fn an_in_flight_assert_straddling_a_mark_answers_live_not_dead() {
let (gate_tx, gate_rx) = tokio::sync::oneshot::channel();
let (arrived_tx, mut arrived_rx) = tokio::sync::watch::channel(false);
let counter = writes();
let rows: Arc<dyn crate::lfs::LfsRows> = Arc::new(crate::lfs::VersionedLfsRows::new(
Arc::new(GatedAssertLfsRows {
inner: Arc::new(crate::lfs::InMemoryLfsRows::new()),
gate: std::sync::Mutex::new(Some(gate_rx)),
arrived: arrived_tx,
}),
Arc::clone(&counter),
));
let source = LfsMedia::new(rows.clone(), Arc::clone(&counter));
let oid = "ab".repeat(32);
let key = lfs_media_key(&oid).unwrap();
let task = tokio::spawn({
let rows = rows.clone();
async move { rows.assert("r/one", &oid, 10).await }
});
loop {
if *arrived_rx.borrow_and_update() {
break;
}
arrived_rx
.changed()
.await
.expect("the gated assert arrives");
}
let keys = alkblobs::LivenessSource::live_keys(source.as_ref());
assert!(
keys.is_empty(),
"the mark's rows read misses the in-flight (uncommitted) row"
);
let before = counter.load(Ordering::Relaxed);
assert_eq!(
before, 0,
"bump-after-commit: no bump while the write is in flight"
);
gate_tx.send(()).expect("the gate opens");
task.await
.expect("the assert completes")
.expect("the assert commits");
assert_eq!(
counter.load(Ordering::Relaxed),
1,
"the bump follows the landed row"
);
assert!(
contains_of(&source, &key),
"just-referenced media answers live — never exact-dead"
);
}
struct GatedApplyRefTable {
inner: Arc<InMemoryRefTable>,
gate: std::sync::Mutex<Option<tokio::sync::oneshot::Receiver<()>>>,
arrived: tokio::sync::watch::Sender<bool>,
}
#[async_trait::async_trait]
impl GitRefs for GatedApplyRefTable {
async fn list_refs(&self, repo: &RepoRecord) -> Result<Vec<alkgit::RefLine>, StorageError> {
self.inner.list_refs(repo).await
}
async fn apply_updates(
&self,
repo: &RepoRecord,
updates: Vec<RefUpdate>,
atomic: bool,
) -> Result<Vec<alkgit::RefOutcome>, StorageError> {
let rx = self.gate.lock().unwrap_or_else(|e| e.into_inner()).take();
if let Some(rx) = rx {
let _ = self.arrived.send(true);
let _ = rx.await;
}
self.inner.apply_updates(repo, updates, atomic).await
}
}
#[async_trait::async_trait]
impl super::RefRows for GatedApplyRefTable {
async fn snapshot(&self) -> Result<super::RefRowsSnapshot, StorageError> {
self.inner.snapshot().await
}
}
#[tokio::test(flavor = "multi_thread")]
async fn an_in_flight_ref_write_straddling_a_mark_answers_live_not_dead() {
let front = ObjectFront::new(Arc::new(Store::mem_only()));
let tip = put_object(&front, Kind::Blob, b"straddled\n").await;
let key = oid_key(&tip).expect("the blob oid's family key");
let (gate_tx, gate_rx) = tokio::sync::oneshot::channel();
let (arrived_tx, mut arrived_rx) = tokio::sync::watch::channel(false);
let counter = writes();
let wrapper = Arc::new(crate::refs::VersionedRefTable::new(
Arc::new(GatedApplyRefTable {
inner: Arc::new(InMemoryRefTable::new()),
gate: std::sync::Mutex::new(Some(gate_rx)),
arrived: arrived_tx,
}),
Arc::clone(&counter),
));
let rows: Arc<dyn super::RefRows> = wrapper.clone();
let source = RefClosure::new(front.clone(), rows, Arc::clone(&counter));
let task = tokio::spawn({
let wrapper = wrapper.clone();
async move {
wrapper
.apply_updates(
&repo("r/one"),
vec![RefUpdate {
name: "refs/heads/main".to_owned(),
expected: None,
new: Some(tip),
}],
false,
)
.await
}
});
loop {
if *arrived_rx.borrow_and_update() {
break;
}
arrived_rx.changed().await.expect("the gated write arrives");
}
let keys = alkblobs::LivenessSource::live_keys(source.as_ref());
assert!(
keys.is_empty(),
"the snapshot read misses the in-flight write"
);
assert_eq!(
counter.load(Ordering::Relaxed),
0,
"bump-after-commit: no bump while the write is in flight"
);
gate_tx.send(()).expect("the gate opens");
task.await
.expect("the write completes")
.expect("the write commits");
assert_eq!(
counter.load(Ordering::Relaxed),
1,
"the bump follows the row"
);
assert!(
alkblobs::LivenessSource::contains(source.as_ref(), &key),
"just-pushed refs' objects answer live — never exact-dead"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn the_registered_media_source_serves_the_sweep_orphan_reclaim() {
let (store, rows, counter) = media_fixture();
let (oid_ref, _) = put_media(&store, b"referenced\n").await;
let (oid_orphan, _) = put_media(&store, b"orphan\n").await;
rows.assert("r/one", &oid_ref, 10).await.unwrap();
let source = LfsMedia::new(rows, Arc::clone(&counter));
source.register(&store).unwrap();
let report = store.sweep().await.unwrap();
assert!(
report.aborted.is_none(),
"a registered source never trips NoLivenessSources"
);
assert_eq!(
report.deleted, 1,
"the never-asserted media reclaims; the referenced survives"
);
let stat = |oid_hex: &str| {
let hex = oid_hex.to_owned();
let store = Arc::clone(&store);
async move {
store
.stat(&lfs_media_key(&hex).unwrap().as_bytes())
.await
.unwrap()
.is_some()
}
};
assert!(stat(&oid_ref).await, "the referenced media stays");
assert!(!stat(&oid_orphan).await, "the orphan is gone");
}
#[cfg(feature = "sqlite")]
#[tokio::test(flavor = "multi_thread")]
async fn the_composition_wiring_serves_the_lfs_sweep_across_the_durable_window() {
use std::time::Duration;
use alkgit::GitRegistryStore as _;
use crate::sqlite::SqliteRegistryAssembly;
let dir = scratch_dir("liveness", "media-wiring");
let db = dir.join("lfs-sweep.sqlite3");
let assembly = SqliteRegistryAssembly::open(&db).await.unwrap();
let store = Arc::new(
Store::kv_only_sqlite(&db, 128 * 1024, Duration::ZERO)
.await
.unwrap(),
);
let lfs_version = assembly.lfs_version().clone();
let rows: Arc<dyn crate::lfs::LfsRows> = assembly.lfs_rows().clone();
let source = super::LfsMedia::new(rows.clone(), Arc::clone(&lfs_version));
source.register(&store).unwrap();
async fn pool_has(store: &Store, hex: &str) -> bool {
store
.stat(&lfs_media_key(hex).unwrap().as_bytes())
.await
.unwrap()
.is_some()
}
let put = |store: Arc<Store>, content: &'static [u8]| async move {
let (hex, key) = put_media(&store, content).await;
(hex, key)
};
let (shared_oid, _) = put(Arc::clone(&store), b"shared\n").await;
rows.assert("repo-a", &shared_oid, 16).await.unwrap();
rows.assert("repo-b", &shared_oid, 16).await.unwrap();
let first = store.sweep().await.unwrap();
assert!(first.aborted.is_none());
assert_eq!(
first.candidates_staged, 0,
"the cross-referenced media never stages while any row stands"
);
assert_eq!(first.deleted, 0);
rows.remove("repo-a", &shared_oid).await.unwrap();
let second = store.sweep().await.unwrap();
assert!(
second.deleted == 0 && second.candidates_staged == 0,
"one row removed — the survivor re-protects the key across the durable window"
);
assert!(pool_has(&store, &shared_oid).await);
rows.remove("repo-b", &shared_oid).await.unwrap();
let third = store.sweep().await.unwrap();
assert_eq!(
third.candidates_staged, 1,
"the last row's removal makes the media sweep-eligible — staged into the window"
);
assert_eq!(third.deleted, 0);
assert!(pool_has(&store, &shared_oid).await, "staged, not deleted");
let fourth = store.sweep().await.unwrap();
assert_eq!(
fourth.deleted, 1,
"the overdue candidate executes — the arbitration's index answered exact"
);
assert!(!pool_has(&store, &shared_oid).await);
let (late_oid, _) = put(Arc::clone(&store), b"late orphan\n").await;
let fifth = store.sweep().await.unwrap();
assert_eq!(fifth.candidates_staged, 1, "the bare orphan stages");
rows.assert("repo-c", &late_oid, 16).await.unwrap();
let sixth = store.sweep().await.unwrap();
assert_eq!(
sixth.cancelled_by_arbitration, 1,
"the row landed across the window — the moved counter cancels the \
candidate (the dangerous direction the counter exists for)"
);
assert!(pool_has(&store, &late_oid).await, "the media survives");
let seventh = store.sweep().await.unwrap();
assert_eq!(
seventh.deleted, 0,
"the mark's rebuild-per-mark saw the row — protected while it stands"
);
let (bare_oid, _) = put(Arc::clone(&store), b"bare\n").await;
let eighth = store.sweep().await.unwrap();
assert_eq!(
eighth.candidates_staged, 1,
"the never-asserted orphan stages"
);
let ninth = store.sweep().await.unwrap();
assert_eq!(
ninth.deleted, 1,
"orphan reclaim across the durable window (POC-L's datum)"
);
assert!(!pool_has(&store, &bare_oid).await);
let (victim_oid, _) = put(Arc::clone(&store), b"victim media\n").await;
use alkgit::RepoRecord;
assembly
.backend()
.put_repo(&RepoRecord {
repo_id: "victim".to_owned(),
visibility: alkgit::Visibility::Private,
grants: Default::default(),
})
.await
.unwrap();
rows.assert("victim", &victim_oid, 16).await.unwrap();
let tenth = store.sweep().await.unwrap();
assert_eq!(
tenth.candidates_staged, 0,
"the repo-referenced media re-protects"
);
assembly.backend().remove_repo("victim").await.unwrap();
let eleventh = store.sweep().await.unwrap();
assert_eq!(
eleventh.candidates_staged, 1,
"the repo deletion's rows leg leaves the media unreferenced — staged"
);
let twelfth = store.sweep().await.unwrap();
assert_eq!(
twelfth.deleted, 1,
"nothing LFS-specific remains — the sweep reclaims"
);
std::fs::remove_dir_all(&dir).unwrap_or_default();
}
}