use std::collections::BTreeMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use alkgit::StorageError;
use async_trait::async_trait;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LfsRow {
pub repo_id: String,
pub oid: String,
pub size: u64,
pub refcount: u64,
}
#[async_trait]
pub trait LfsRows: Send + Sync {
async fn assert(&self, repo_id: &str, oid: &str, size: u64) -> Result<(), StorageError>;
async fn remove(&self, repo_id: &str, oid: &str) -> Result<(), StorageError>;
async fn remove_repo(&self, repo_id: &str) -> Result<(), StorageError>;
async fn rows(&self) -> Result<Vec<LfsRow>, StorageError>;
async fn repo_rows(&self, repo_id: &str) -> Result<Vec<LfsRow>, StorageError>;
}
fn validated_repo_id(repo_id: &str) -> Result<(), StorageError> {
if repo_id.is_empty() {
return Err(StorageError::Invalid(
"lfs reference assertion: the repo id is empty (the registry id is resolved per \
call)"
.to_owned(),
));
}
Ok(())
}
fn validated_oid(oid: &str) -> Result<(), StorageError> {
let hex_ok = oid.len() == 64 && oid.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'));
if !hex_ok {
return Err(StorageError::Invalid(format!(
"lfs reference assertion: the oid is not a 64-lowercase-hex sha256 form: {oid:?}"
)));
}
Ok(())
}
fn validated_size(size: u64) -> Result<(), StorageError> {
if size > i64::MAX as u64 {
return Err(StorageError::Invalid(format!(
"lfs reference assertion: the size exceeds the engine integer range: {size}"
)));
}
Ok(())
}
pub struct InMemoryLfsRows {
state: tokio::sync::RwLock<InMemoryLfsState>,
}
#[derive(Default)]
struct InMemoryLfsState {
rows: BTreeMap<(String, String), u64>,
refcounts: BTreeMap<String, u64>,
}
impl Default for InMemoryLfsRows {
fn default() -> Self {
Self::new()
}
}
impl InMemoryLfsRows {
pub fn new() -> Self {
InMemoryLfsRows {
state: tokio::sync::RwLock::new(InMemoryLfsState::default()),
}
}
fn recount(rows: &BTreeMap<(String, String), u64>, oid: &str) -> u64 {
rows.keys().filter(|(_, o)| o == oid).count() as u64
}
}
#[async_trait]
impl LfsRows for InMemoryLfsRows {
async fn assert(&self, repo_id: &str, oid: &str, size: u64) -> Result<(), StorageError> {
validated_repo_id(repo_id)?;
validated_oid(oid)?;
validated_size(size)?;
let mut state = self.state.write().await;
let newly = state
.rows
.insert((repo_id.to_owned(), oid.to_owned()), size)
.is_none();
if newly {
let count = Self::recount(&state.rows, oid);
state.refcounts.insert(oid.to_owned(), count);
}
Ok(())
}
async fn remove(&self, repo_id: &str, oid: &str) -> Result<(), StorageError> {
validated_repo_id(repo_id)?;
validated_oid(oid)?;
let mut state = self.state.write().await;
if state
.rows
.remove(&(repo_id.to_owned(), oid.to_owned()))
.is_some()
{
let count = Self::recount(&state.rows, oid);
if count == 0 {
state.refcounts.remove(oid);
} else {
state.refcounts.insert(oid.to_owned(), count);
}
}
Ok(())
}
async fn remove_repo(&self, repo_id: &str) -> Result<(), StorageError> {
validated_repo_id(repo_id)?;
let mut state = self.state.write().await;
let affected: Vec<String> = state
.rows
.keys()
.filter(|(r, _)| r == repo_id)
.map(|(_, o)| o.clone())
.collect();
state.rows.retain(|r, _| r.0 != repo_id);
for oid in affected {
let count = Self::recount(&state.rows, &oid);
if count == 0 {
state.refcounts.remove(&oid);
} else {
state.refcounts.insert(oid, count);
}
}
Ok(())
}
async fn rows(&self) -> Result<Vec<LfsRow>, StorageError> {
Ok(self.read_all().await)
}
async fn repo_rows(&self, repo_id: &str) -> Result<Vec<LfsRow>, StorageError> {
validated_repo_id(repo_id)?;
Ok(self
.read_all()
.await
.into_iter()
.filter(|row| row.repo_id == repo_id)
.collect())
}
}
impl InMemoryLfsRows {
async fn read_all(&self) -> Vec<LfsRow> {
let state = self.state.read().await;
state
.rows
.iter()
.map(|((repo_id, oid), size)| LfsRow {
repo_id: repo_id.clone(),
oid: oid.clone(),
size: *size,
refcount: state.refcounts.get(oid).copied().unwrap_or(0),
})
.collect()
}
}
#[cfg(feature = "sqlite")]
pub struct SqliteLfsRows {
handle: crate::migrations::CoTenantSqlite,
}
#[cfg(feature = "sqlite")]
impl SqliteLfsRows {
pub fn new(handle: crate::migrations::CoTenantSqlite) -> Self {
SqliteLfsRows { handle }
}
}
#[cfg(feature = "sqlite")]
mod sqlite_twin {
use super::*;
fn storage_io(context: &str, error: rusqlite::Error) -> StorageError {
StorageError::Io(format!("{context}: {error}"))
}
fn recount_sql() -> &'static str {
"UPDATE lfs_objects \
SET refcount = (SELECT count(*) FROM lfs_objects AS c WHERE c.oid = lfs_objects.oid) \
WHERE oid = ?1"
}
fn recount_all_sql() -> &'static str {
"UPDATE lfs_objects \
SET refcount = (SELECT count(*) FROM lfs_objects AS c WHERE c.oid = lfs_objects.oid)"
}
fn read_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<LfsRow> {
Ok(LfsRow {
repo_id: row.get(0)?,
oid: row.get(1)?,
size: row.get::<_, i64>(2)? as u64,
refcount: row.get::<_, i64>(3)? as u64,
})
}
async fn run_blocking<T: Send + 'static>(
handle: crate::migrations::CoTenantSqlite,
work: impl FnOnce(&mut rusqlite::Connection) -> Result<T, StorageError> + Send + 'static,
) -> Result<T, StorageError> {
tokio::task::spawn_blocking(move || {
let mut conn = handle.lock();
work(&mut conn)
})
.await
.map_err(|e| StorageError::Io(format!("lfs rows blocking task: {e}")))?
}
async fn run_transaction<T: Send + 'static>(
handle: crate::migrations::CoTenantSqlite,
work: impl FnOnce(&rusqlite::Transaction<'_>) -> Result<T, StorageError> + Send + 'static,
) -> Result<T, StorageError> {
run_blocking(handle, move |conn| {
let tx = conn
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
.map_err(|e| storage_io("lfs rows BEGIN IMMEDIATE", e))?;
let value = work(&tx)?;
tx.commit().map_err(|e| storage_io("lfs rows commit", e))?;
Ok(value)
})
.await
}
#[async_trait]
impl LfsRows for SqliteLfsRows {
async fn assert(&self, repo_id: &str, oid: &str, size: u64) -> Result<(), StorageError> {
validated_repo_id(repo_id)?;
validated_oid(oid)?;
validated_size(size)?;
let handle = self.handle.clone();
let (repo_id, oid) = (repo_id.to_owned(), oid.to_owned());
run_transaction(handle, move |tx| {
tx.execute(
"INSERT INTO lfs_objects (repo_id, oid, size, refcount) \
VALUES (?1, ?2, ?3, 1) \
ON CONFLICT(repo_id, oid) DO UPDATE SET size = excluded.size",
rusqlite::params![repo_id, oid, size as i64],
)
.map_err(|e| storage_io("lfs assert upsert", e))?;
tx.execute(recount_sql(), rusqlite::params![oid])
.map_err(|e| storage_io("lfs assert refcount recount", e))?;
Ok(())
})
.await
}
async fn remove(&self, repo_id: &str, oid: &str) -> Result<(), StorageError> {
validated_repo_id(repo_id)?;
validated_oid(oid)?;
let handle = self.handle.clone();
let (repo_id, oid) = (repo_id.to_owned(), oid.to_owned());
run_transaction(handle, move |tx| {
tx.execute(
"DELETE FROM lfs_objects WHERE repo_id = ?1 AND oid = ?2",
rusqlite::params![repo_id, oid],
)
.map_err(|e| storage_io("lfs reference remove", e))?;
tx.execute(recount_sql(), rusqlite::params![oid])
.map_err(|e| storage_io("lfs remove refcount recount", e))?;
Ok(())
})
.await
}
async fn remove_repo(&self, repo_id: &str) -> Result<(), StorageError> {
validated_repo_id(repo_id)?;
let handle = self.handle.clone();
let repo_id = repo_id.to_owned();
run_transaction(handle, move |tx| {
tx.execute(
"DELETE FROM lfs_objects WHERE repo_id = ?1",
rusqlite::params![repo_id],
)
.map_err(|e| storage_io("lfs repo removal", e))?;
tx.execute(recount_all_sql(), [])
.map_err(|e| storage_io("lfs repo removal recount", e))?;
Ok(())
})
.await
}
async fn rows(&self) -> Result<Vec<LfsRow>, StorageError> {
let handle = self.handle.clone();
run_blocking(handle, move |conn| {
let mut stmt = conn
.prepare(
"SELECT repo_id, oid, size, refcount FROM lfs_objects \
ORDER BY repo_id, oid",
)
.map_err(|e| storage_io("lfs rows listing prepare", e))?;
let rows = stmt
.query_map([], read_row)
.map_err(|e| storage_io("lfs rows listing query", e))?;
rows.collect::<Result<Vec<_>, _>>()
.map_err(|e| storage_io("lfs rows listing row", e))
})
.await
}
async fn repo_rows(&self, repo_id: &str) -> Result<Vec<LfsRow>, StorageError> {
validated_repo_id(repo_id)?;
let handle = self.handle.clone();
let repo_id = repo_id.to_owned();
run_blocking(handle, move |conn| {
let mut stmt = conn
.prepare(
"SELECT repo_id, oid, size, refcount FROM lfs_objects \
WHERE repo_id = ?1 ORDER BY oid",
)
.map_err(|e| storage_io("lfs rows listing prepare", e))?;
let rows = stmt
.query_map(rusqlite::params![repo_id], read_row)
.map_err(|e| storage_io("lfs rows listing query", e))?;
rows.collect::<Result<Vec<_>, _>>()
.map_err(|e| storage_io("lfs rows listing row", e))
})
.await
}
}
}
#[cfg(feature = "postgres")]
pub struct PgLfsRows {
handle: crate::postgres::CoTenantPg,
}
#[cfg(feature = "postgres")]
impl PgLfsRows {
pub fn new(handle: crate::postgres::CoTenantPg) -> Self {
PgLfsRows { handle }
}
}
#[cfg(feature = "postgres")]
mod pg_twin {
use super::*;
use crate::migrations::{lfs_objects_lock_key, qualify};
fn pg_io(context: &str, error: tokio_postgres::Error) -> StorageError {
StorageError::Io(format!("{context}: {error}"))
}
fn recount_sql(table: &str) -> String {
format!(
"UPDATE {table} AS t0 \
SET refcount = (SELECT count(*) FROM {table} c WHERE c.oid = t0.oid) \
WHERE t0.oid = $1"
)
}
fn recount_all_sql(table: &str) -> String {
format!(
"UPDATE {table} AS t0 \
SET refcount = (SELECT count(*) FROM {table} c WHERE c.oid = t0.oid)"
)
}
#[async_trait]
impl LfsRows for PgLfsRows {
async fn assert(&self, repo_id: &str, oid: &str, size: u64) -> Result<(), StorageError> {
validated_repo_id(repo_id)?;
validated_oid(oid)?;
validated_size(size)?;
let schema = self.handle.schema().to_owned();
let size = size as i64;
let (repo_id, oid) = (repo_id.to_owned(), oid.to_owned());
let mut guard = self.handle.lock().await;
let tx = guard
.transaction()
.await
.map_err(|e| pg_io("lfs rows BEGIN", e))?;
tx.execute(
"SELECT pg_advisory_xact_lock($1)",
&[&lfs_objects_lock_key(&schema)],
)
.await
.map_err(|e| pg_io("lfs rows advisory lock", e))?;
let upsert = format!(
"INSERT INTO {} (repo_id, oid, size, refcount) \
VALUES ($1, $2, $3, 1) \
ON CONFLICT (repo_id, oid) DO UPDATE SET size = excluded.size",
qualify(&schema, "lfs_objects")
);
tx.execute(&upsert, &[&repo_id, &oid, &size])
.await
.map_err(|e| pg_io("lfs assert upsert", e))?;
tx.execute(&recount_sql(&qualify(&schema, "lfs_objects")), &[&oid])
.await
.map_err(|e| pg_io("lfs assert refcount recount", e))?;
tx.commit().await.map_err(|e| pg_io("lfs rows commit", e))?;
Ok(())
}
async fn remove(&self, repo_id: &str, oid: &str) -> Result<(), StorageError> {
validated_repo_id(repo_id)?;
validated_oid(oid)?;
let schema = self.handle.schema().to_owned();
let (repo_id, oid) = (repo_id.to_owned(), oid.to_owned());
let mut guard = self.handle.lock().await;
let tx = guard
.transaction()
.await
.map_err(|e| pg_io("lfs rows BEGIN", e))?;
tx.execute(
"SELECT pg_advisory_xact_lock($1)",
&[&lfs_objects_lock_key(&schema)],
)
.await
.map_err(|e| pg_io("lfs rows advisory lock", e))?;
let delete = format!(
"DELETE FROM {} WHERE repo_id = $1 AND oid = $2",
qualify(&schema, "lfs_objects")
);
tx.execute(&delete, &[&repo_id, &oid])
.await
.map_err(|e| pg_io("lfs reference remove", e))?;
tx.execute(&recount_sql(&qualify(&schema, "lfs_objects")), &[&oid])
.await
.map_err(|e| pg_io("lfs remove refcount recount", e))?;
tx.commit().await.map_err(|e| pg_io("lfs rows commit", e))?;
Ok(())
}
async fn remove_repo(&self, repo_id: &str) -> Result<(), StorageError> {
validated_repo_id(repo_id)?;
let schema = self.handle.schema().to_owned();
let repo_id = repo_id.to_owned();
let mut guard = self.handle.lock().await;
let tx = guard
.transaction()
.await
.map_err(|e| pg_io("lfs rows BEGIN", e))?;
tx.execute(
"SELECT pg_advisory_xact_lock($1)",
&[&lfs_objects_lock_key(&schema)],
)
.await
.map_err(|e| pg_io("lfs rows advisory lock", e))?;
let delete = format!(
"DELETE FROM {} WHERE repo_id = $1",
qualify(&schema, "lfs_objects")
);
tx.execute(&delete, &[&repo_id])
.await
.map_err(|e| pg_io("lfs repo removal", e))?;
let recount = recount_all_sql(&qualify(&schema, "lfs_objects"));
tx.execute(&recount, &[])
.await
.map_err(|e| pg_io("lfs repo removal recount", e))?;
tx.commit().await.map_err(|e| pg_io("lfs rows commit", e))?;
Ok(())
}
async fn rows(&self) -> Result<Vec<LfsRow>, StorageError> {
let select = format!(
"SELECT repo_id, oid, size, refcount FROM {} ORDER BY repo_id, oid",
qualify(self.handle.schema(), "lfs_objects")
);
let guard = self.handle.lock().await;
let rows = guard
.query(&select, &[])
.await
.map_err(|e| pg_io("lfs rows listing query", e))?;
flatten_rows(&rows)
}
async fn repo_rows(&self, repo_id: &str) -> Result<Vec<LfsRow>, StorageError> {
validated_repo_id(repo_id)?;
let select = format!(
"SELECT repo_id, oid, size, refcount FROM {} WHERE repo_id = $1 ORDER BY oid",
qualify(self.handle.schema(), "lfs_objects")
);
let guard = self.handle.lock().await;
let rows = guard
.query(&select, &[&repo_id])
.await
.map_err(|e| pg_io("lfs rows listing query", e))?;
flatten_rows(&rows)
}
}
fn flatten_rows(rows: &[tokio_postgres::Row]) -> Result<Vec<LfsRow>, StorageError> {
rows.iter()
.map(|row| {
Ok(LfsRow {
repo_id: row
.try_get(0)
.map_err(|e| pg_io("lfs rows listing row", e))?,
oid: row
.try_get(1)
.map_err(|e| pg_io("lfs rows listing row", e))?,
size: row
.try_get::<_, i64>(2)
.map_err(|e| pg_io("lfs rows listing row", e))?
as u64,
refcount: row
.try_get::<_, i64>(3)
.map_err(|e| pg_io("lfs rows listing row", e))?
as u64,
})
})
.collect()
}
}
#[cfg(test)]
pub(crate) mod battery {
use super::*;
pub(crate) fn oid_hex(tag: u8) -> String {
let base = format!("{tag:02x}").repeat(32);
base
}
pub(crate) async fn assert_and_the_refcount_aware_upsert(
table: &dyn LfsRows,
) -> Result<(), StorageError> {
table.assert("repo/a", &oid_hex(1), 128).await?;
table.assert("repo/a", &oid_hex(1), 128).await?;
assert_eq!(
table.repo_rows("repo/a").await?,
vec![LfsRow {
repo_id: "repo/a".to_owned(),
oid: oid_hex(1),
size: 128,
refcount: 1,
}],
"the same oid re-asserted in one repo is one row"
);
table.assert("repo/a", &oid_hex(1), 256).await?;
assert_eq!(
table.repo_rows("repo/a").await?[0].size,
256,
"the re-assert refreshes the row's size"
);
assert_eq!(table.repo_rows("repo/a").await?[0].refcount, 1);
table.assert("repo/b", &oid_hex(1), 256).await?;
let shared: Vec<LfsRow> = table
.rows()
.await?
.into_iter()
.filter(|row| row.oid == oid_hex(1))
.collect();
assert_eq!(shared.len(), 2);
for row in &shared {
assert_eq!(
row.refcount, 2,
"every row of an oid reads the same count: {row:?}"
);
}
Ok(())
}
pub(crate) async fn cross_reference_reprotection(
table: &dyn LfsRows,
) -> Result<(), StorageError> {
table.assert("repo/one", &oid_hex(2), 64).await?;
table.assert("repo/two", &oid_hex(2), 64).await?;
assert_eq!(
table
.rows()
.await?
.into_iter()
.filter(|row| row.oid == oid_hex(2))
.count(),
2
);
table.remove("repo/one", &oid_hex(2)).await?;
let survivor = table.repo_rows("repo/two").await?;
assert_eq!(
survivor,
vec![LfsRow {
repo_id: "repo/two".to_owned(),
oid: oid_hex(2),
size: 64,
refcount: 1,
}],
"one row removed — the survivor carries the decremented count; the key stays referenced"
);
table.remove("repo/two", &oid_hex(2)).await?;
assert!(
table
.rows()
.await?
.into_iter()
.all(|row| row.oid != oid_hex(2)),
"all rows removed — nothing references the key (sweep-eligible)"
);
Ok(())
}
pub(crate) async fn removal_of_an_absent_row_is_a_noop(
table: &dyn LfsRows,
) -> Result<(), StorageError> {
table.assert("repo/noop", &oid_hex(3), 16).await?;
table.remove("repo/absent", &oid_hex(3)).await?;
table.remove("repo/noop", &oid_hex(0x3f)).await?;
let rows = table.repo_rows("repo/noop").await?;
assert_eq!(rows.len(), 1);
assert_eq!(
rows[0].refcount, 1,
"the no-op removals left the count exact"
);
Ok(())
}
pub(crate) async fn repo_deletion_takes_the_rows_and_recounts(
table: &dyn LfsRows,
) -> Result<(), StorageError> {
table.assert("victim", &oid_hex(4), 32).await?;
table.assert("victim", &oid_hex(5), 48).await?;
table.assert("survivor", &oid_hex(4), 32).await?;
table.assert("survivor", &oid_hex(9), 72).await?;
table.remove_repo("victim").await?;
let touched: Vec<LfsRow> = table
.rows()
.await?
.into_iter()
.filter(|row| row.repo_id == "survivor" || row.repo_id == "victim")
.collect();
assert_eq!(
touched,
vec![
LfsRow {
repo_id: "survivor".to_owned(),
oid: oid_hex(4),
size: 32,
refcount: 1,
},
LfsRow {
repo_id: "survivor".to_owned(),
oid: oid_hex(9),
size: 72,
refcount: 1,
},
],
"the victim's rows went with the repo; the survivor's shared oid recomputed to 1"
);
table.assert("victim", &oid_hex(4), 32).await?;
assert_eq!(
table.repo_rows("victim").await?[0].refcount,
2,
"a shared oid re-referenced by the former victim counts both rows again"
);
Ok(())
}
pub(crate) async fn the_reads_list_canonically(
table: &dyn LfsRows,
) -> Result<(), StorageError> {
table.assert("repo/z", &oid_hex(6), 8).await?;
table.assert("repo/z", &oid_hex(7), 9).await?;
table.assert("repo/a", &oid_hex(8), 10).await?;
let all = table.rows().await?;
let keys: Vec<(String, String)> = all
.iter()
.map(|r| (r.repo_id.clone(), r.oid.clone()))
.collect();
let mut sorted = keys.clone();
sorted.sort();
assert_eq!(
keys, sorted,
"all rows arrive in canonical (repo_id, oid) order"
);
let mine = table.repo_rows("repo/z").await?;
assert_eq!(
mine.iter().map(|r| r.oid.clone()).collect::<Vec<_>>(),
vec![oid_hex(6), oid_hex(7)],
"the per-repo view filters and stays oid-ordered"
);
Ok(())
}
pub(crate) async fn validation_rejects_as_storage_invalid(
table: &dyn LfsRows,
) -> Result<(), StorageError> {
let before = table.rows().await?.len();
for oid in [
"",
"nothex",
&"AB".repeat(32),
&"1".repeat(63),
&"1".repeat(65),
] {
assert!(
matches!(
table.assert("repo/a", oid, 8).await,
Err(StorageError::Invalid(_))
),
"the oid shape is a table-layer input: {oid:?}"
);
}
assert!(
matches!(
table.assert("", &oid_hex(1), 8).await,
Err(StorageError::Invalid(_))
),
"an empty repo id is table-layer invalid"
);
assert!(
matches!(
table
.assert("repo/a", &oid_hex(1), (i64::MAX as u64) + 1)
.await,
Err(StorageError::Invalid(_))
),
"a size over the engine integer range is table-layer invalid"
);
assert_eq!(
table.rows().await?.len(),
before,
"the rejected inputs wrote nothing"
);
Ok(())
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "sqlite")]
use std::sync::Arc;
use super::battery;
use super::*;
#[cfg(feature = "sqlite")]
use crate::testutil::scratch_dir;
async fn in_memory_reference() -> InMemoryLfsRows {
InMemoryLfsRows::new()
}
#[tokio::test]
async fn the_in_memory_reference_runs_the_battery() {
let table = in_memory_reference().await;
battery::assert_and_the_refcount_aware_upsert(&table)
.await
.unwrap();
battery::cross_reference_reprotection(&table).await.unwrap();
battery::removal_of_an_absent_row_is_a_noop(&table)
.await
.unwrap();
battery::repo_deletion_takes_the_rows_and_recounts(&table)
.await
.unwrap();
battery::the_reads_list_canonically(&table).await.unwrap();
battery::validation_rejects_as_storage_invalid(&table)
.await
.unwrap();
}
#[tokio::test]
async fn the_in_memory_refcounts_track_the_rows() {
let table = in_memory_reference().await;
let refcount_of = |rows: &Vec<LfsRow>, repo: &str| -> u64 {
rows.iter()
.find(|row| row.repo_id == repo)
.map(|row| row.refcount)
.expect("the row exists")
};
table.assert("r", &battery::oid_hex(1), 8).await.unwrap();
table.assert("r", &battery::oid_hex(1), 8).await.unwrap();
assert_eq!(
refcount_of(&table.rows().await.unwrap(), "r"),
1,
"the re-assert did not bump the count"
);
table.assert("r2", &battery::oid_hex(1), 8).await.unwrap();
assert_eq!(refcount_of(&table.rows().await.unwrap(), "r2"), 2);
table.remove("r", &battery::oid_hex(1)).await.unwrap();
assert_eq!(refcount_of(&table.rows().await.unwrap(), "r2"), 1);
table.remove("r2", &battery::oid_hex(1)).await.unwrap();
assert!(
table.rows().await.unwrap().is_empty(),
"the last removal leaves no row, no refcount entry"
);
}
#[tokio::test]
async fn the_versioned_wrapper_forwards_and_bumps_on_assert() {
let inner = Arc::new(InMemoryLfsRows::new());
let counter = Arc::new(AtomicU64::new(0));
let wrapped = VersionedLfsRows::new(inner.clone(), Arc::clone(&counter));
let count = || counter.load(Ordering::Relaxed);
battery::assert_and_the_refcount_aware_upsert(&wrapped)
.await
.unwrap();
assert_eq!(
count(),
4,
"every assert bumps — same-pair re-asserts included"
);
let before = count();
battery::cross_reference_reprotection(&wrapped)
.await
.unwrap();
assert_eq!(
count() - before,
2,
"the two asserts bumped; the two removals read back without one"
);
let before = count();
battery::removal_of_an_absent_row_is_a_noop(&wrapped)
.await
.unwrap();
assert_eq!(
count() - before,
1,
"the one assert bumped — the no-op removals did not"
);
let before = count();
battery::the_reads_list_canonically(&wrapped).await.unwrap();
assert_eq!(count() - before, 3, "the reads themselves do not bump");
let before = count();
battery::validation_rejects_as_storage_invalid(&wrapped)
.await
.unwrap();
assert_eq!(
count() - before,
0,
"the rejected asserts bump nothing (no rows landed — the bump-after-commit discipline)"
);
let before = count();
battery::removal_of_an_absent_row_is_a_noop(&wrapped)
.await
.unwrap();
assert_eq!(count(), before + 1, "removals never bump");
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn the_sqlite_twin_runs_the_battery() {
let dir = scratch_dir("lfs", "sqlite-battery");
let handle = crate::migrations::CoTenantSqlite::open(dir.join("rows.sqlite3")).unwrap();
let table = SqliteLfsRows::new(handle);
battery::assert_and_the_refcount_aware_upsert(&table)
.await
.unwrap();
battery::cross_reference_reprotection(&table).await.unwrap();
battery::removal_of_an_absent_row_is_a_noop(&table)
.await
.unwrap();
battery::repo_deletion_takes_the_rows_and_recounts(&table)
.await
.unwrap();
battery::the_reads_list_canonically(&table).await.unwrap();
battery::validation_rejects_as_storage_invalid(&table)
.await
.unwrap();
std::fs::remove_dir_all(&dir).unwrap_or_default();
}
#[cfg(feature = "sqlite")]
#[tokio::test(flavor = "multi_thread")]
async fn the_sqlite_concurrent_asserts_converge_on_exact_counts() {
let dir = scratch_dir("lfs", "sqlite-race");
let path = dir.join("race.sqlite3");
let first = Arc::new(SqliteLfsRows::new(
crate::migrations::CoTenantSqlite::open(&path).unwrap(),
));
let second = Arc::new(SqliteLfsRows::new(
crate::migrations::CoTenantSqlite::open(&path).unwrap(),
));
let oid = battery::oid_hex(2);
let mut joins = Vec::new();
for store in [first.clone(), second.clone()] {
for repo in ["repo/a", "repo/b", "repo/c", "repo/d"] {
joins.push(tokio::spawn({
let store = store.clone();
let oid = oid.clone();
let repo = repo.to_owned();
async move { store.assert(&repo, &oid, 64).await }
}));
}
}
for join in joins {
join.await.unwrap().expect("every assert lands");
}
let rows = first.rows().await.unwrap();
assert_eq!(rows.len(), 4, "four repos — four rows, one key");
for row in &rows {
assert_eq!(
row.refcount, 4,
"every row carries the exact count: {row:?}"
);
}
std::fs::remove_dir_all(&dir).unwrap_or_default();
}
}
pub struct VersionedLfsRows<T: ?Sized> {
inner: Arc<T>,
version: Arc<AtomicU64>,
}
impl<T: ?Sized> VersionedLfsRows<T> {
pub fn new(inner: Arc<T>, version: Arc<AtomicU64>) -> Self {
VersionedLfsRows { inner, version }
}
pub fn inner(&self) -> &Arc<T> {
&self.inner
}
pub fn version(&self) -> u64 {
self.version.load(Ordering::Relaxed)
}
}
#[async_trait]
impl<T> LfsRows for VersionedLfsRows<T>
where
T: LfsRows + ?Sized,
{
async fn assert(&self, repo_id: &str, oid: &str, size: u64) -> Result<(), StorageError> {
self.inner.assert(repo_id, oid, size).await?;
self.version.fetch_add(1, Ordering::Relaxed);
Ok(())
}
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<LfsRow>, StorageError> {
self.inner.rows().await
}
async fn repo_rows(&self, repo_id: &str) -> Result<Vec<LfsRow>, StorageError> {
self.inner.repo_rows(repo_id).await
}
}
#[cfg(all(test, feature = "postgres"))]
mod pg_twin_tests {
use std::sync::Arc;
use crate::postgres::live_harness::{drop_schema, live_pg, unique_schema};
use super::battery;
use super::*;
#[tokio::test]
async fn pg_rows_twin_carries_the_battery() {
let dsn = live_pg!();
let schema = unique_schema("lfs-battery");
let table = PgLfsRows::new(
crate::postgres::CoTenantPg::open(&dsn, &schema)
.await
.unwrap(),
);
battery::assert_and_the_refcount_aware_upsert(&table)
.await
.unwrap();
battery::cross_reference_reprotection(&table).await.unwrap();
battery::removal_of_an_absent_row_is_a_noop(&table)
.await
.unwrap();
battery::repo_deletion_takes_the_rows_and_recounts(&table)
.await
.unwrap();
battery::the_reads_list_canonically(&table).await.unwrap();
battery::validation_rejects_as_storage_invalid(&table)
.await
.unwrap();
drop(table);
drop_schema(&dsn, &schema).await;
}
#[tokio::test]
async fn pg_concurrent_asserts_converge_on_exact_counts() {
let dsn = live_pg!();
let schema = unique_schema("lfs-race");
let oid = battery::oid_hex(2);
let mut joins = Vec::new();
for index in 0..4 {
let table = Arc::new(PgLfsRows::new(
crate::postgres::CoTenantPg::open(&dsn, &schema)
.await
.unwrap(),
));
let oid = oid.clone();
joins.push(tokio::spawn(async move {
table
.assert(&format!("repo/{index}"), &oid, 64)
.await
.expect("the assert lands")
}));
}
for join in joins {
join.await.unwrap();
}
let table = PgLfsRows::new(
crate::postgres::CoTenantPg::open(&dsn, &schema)
.await
.unwrap(),
);
for row in table.rows().await.unwrap() {
assert_eq!(
row.refcount, 4,
"every row carries the exact count: {row:?}"
);
}
drop(table);
drop_schema(&dsn, &schema).await;
}
}