#![allow(clippy::too_many_lines)]
use std::sync::Arc;
use alkgit::{RegistryError, RepoRecord};
use async_trait::async_trait;
use crate::error::OpenError;
use crate::migrations::{git_refs_lock_key, lfs_objects_lock_key, qualify};
use crate::reactor::{AnnouncingRegistry, Reactor, RegistryEvent};
use crate::registry::{
row_to_record, validate_record, visibility_str, CachedRegistryProvider, RegistryBackend,
RegistryRow, RegistryWrite, HEAD_SEED_TARGET,
};
pub use crate::migrations::{CoTenantPg, DEFAULT_SCHEMA};
fn io_err(context: &str, error: tokio_postgres::Error) -> RegistryError {
RegistryError::Io(format!("{context}: {error}"))
}
pub struct PgRegistry {
handle: CoTenantPg,
}
impl PgRegistry {
pub fn new(handle: CoTenantPg) -> Self {
PgRegistry { handle }
}
pub(crate) async fn record_mutation(&self, repo_id: &str) -> Result<i64, RegistryError> {
let mut guard = self.handle.lock().await;
durable_record_mutation(&mut guard, self.handle.schema(), repo_id).await
}
}
#[async_trait]
impl RegistryWrite for PgRegistry {
async fn put(&self, record: &RepoRecord) -> Result<i64, RegistryError> {
validate_record(record)?;
let grants = serde_json::to_string(&record.grants).map_err(|_| RegistryError::Invalid)?;
let mut guard = self.handle.lock().await;
durable_put(&mut guard, self.handle.schema(), record, &grants).await
}
async fn update(&self, record: &RepoRecord) -> Result<i64, RegistryError> {
validate_record(record)?;
let grants = serde_json::to_string(&record.grants).map_err(|_| RegistryError::Invalid)?;
let mut guard = self.handle.lock().await;
durable_update(&mut guard, self.handle.schema(), record, &grants).await
}
async fn remove(&self, repo_id: &str) -> Result<i64, RegistryError> {
let mut guard = self.handle.lock().await;
durable_remove(&mut guard, self.handle.schema(), repo_id).await
}
async fn watermark(&self) -> Result<i64, RegistryError> {
let guard = self.handle.lock().await;
durable_watermark(&guard, self.handle.schema()).await
}
async fn rows_since(&self, seq: i64) -> Result<Vec<RegistryRow>, RegistryError> {
let guard = self.handle.lock().await;
durable_rows_since(&guard, self.handle.schema(), seq).await
}
}
fn seq_bump_sql(schema: &str) -> String {
format!(
"UPDATE {} SET value = value + 1 WHERE id = 1 RETURNING value",
qualify(schema, "registry_seq")
)
}
fn seq_of(row: &tokio_postgres::Row, context: &str) -> Result<i64, RegistryError> {
row.try_get(0).map_err(|e| io_err(context, e))
}
async fn durable_put(
client: &mut tokio_postgres::Client,
schema: &str,
record: &RepoRecord,
grants: &str,
) -> Result<i64, RegistryError> {
let bump = seq_bump_sql(schema);
let exists_sql = format!(
"SELECT true FROM {} WHERE repo_id = $1",
qualify(schema, "git_repos")
);
let insert_sql = format!(
"INSERT INTO {} (repo_id, visibility, grants, seq) VALUES ($1, $2, $3, $4)",
qualify(schema, "git_repos")
);
let seed_sql = format!(
"INSERT INTO {} (repo_id, name, oid_hex, symref_target) VALUES ($1, 'HEAD', NULL, $2)",
qualify(schema, "git_refs")
);
let tx = client
.transaction()
.await
.map_err(|e| io_err("registry put BEGIN", e))?;
tx.execute(
"SELECT pg_advisory_xact_lock($1)",
&[&git_refs_lock_key(schema)],
)
.await
.map_err(|e| io_err("registry put git_refs advisory lock", e))?;
let seq: i64 = tx
.query_one(&bump, &[])
.await
.map_err(|e| io_err("registry put seq bump", e))
.and_then(|row| seq_of(&row, "registry put seq bump"))?;
let taken = tx
.query_opt(&exists_sql, &[&record.repo_id])
.await
.map_err(|e| io_err("registry put existence read", e))?;
if taken.is_some() {
tx.rollback().await.ok();
return Err(RegistryError::AlreadyExists);
}
let visibility = visibility_str(record.visibility);
tx.execute(&insert_sql, &[&record.repo_id, &visibility, &grants, &seq])
.await
.map_err(|e| io_err("registry put record row", e))?;
tx.execute(&seed_sql, &[&record.repo_id, &HEAD_SEED_TARGET])
.await
.map_err(|e| io_err("registry put HEAD seed", e))?;
tx.commit()
.await
.map_err(|e| io_err("registry put commit", e))?;
Ok(seq)
}
async fn durable_update(
client: &mut tokio_postgres::Client,
schema: &str,
record: &RepoRecord,
grants: &str,
) -> Result<i64, RegistryError> {
let bump = seq_bump_sql(schema);
let exists_sql = format!(
"SELECT true FROM {} WHERE repo_id = $1",
qualify(schema, "git_repos")
);
let update_sql = format!(
"UPDATE {} SET visibility = $2, grants = $3, seq = $4 WHERE repo_id = $1",
qualify(schema, "git_repos")
);
let tx = client
.transaction()
.await
.map_err(|e| io_err("registry update BEGIN", e))?;
let seq: i64 = tx
.query_one(&bump, &[])
.await
.map_err(|e| io_err("registry update seq bump", e))
.and_then(|row| seq_of(&row, "registry update seq bump"))?;
let exists = tx
.query_opt(&exists_sql, &[&record.repo_id])
.await
.map_err(|e| io_err("registry update existence read", e))?;
if exists.is_none() {
tx.rollback().await.ok();
return Err(RegistryError::NotFound);
}
let visibility = visibility_str(record.visibility);
tx.execute(&update_sql, &[&record.repo_id, &visibility, &grants, &seq])
.await
.map_err(|e| io_err("registry update record row", e))?;
tx.commit()
.await
.map_err(|e| io_err("registry update commit", e))?;
Ok(seq)
}
async fn durable_remove(
client: &mut tokio_postgres::Client,
schema: &str,
repo_id: &str,
) -> Result<i64, RegistryError> {
let bump = seq_bump_sql(schema);
let exists_sql = format!(
"SELECT true FROM {} WHERE repo_id = $1",
qualify(schema, "git_repos")
);
let delete_record = format!(
"DELETE FROM {} WHERE repo_id = $1",
qualify(schema, "git_repos")
);
let delete_refs = format!(
"DELETE FROM {} WHERE repo_id = $1",
qualify(schema, "git_refs")
);
let delete_markers = format!(
"DELETE FROM {} WHERE repo_id = $1",
qualify(schema, "repo_mutation_markers")
);
let delete_lfs = format!(
"DELETE FROM {} WHERE repo_id = $1",
qualify(schema, "lfs_objects")
);
let recount_lfs = format!(
"UPDATE {} AS t0 \
SET refcount = (SELECT count(*) FROM {} c WHERE c.oid = t0.oid)",
qualify(schema, "lfs_objects"),
qualify(schema, "lfs_objects")
);
let tx = client
.transaction()
.await
.map_err(|e| io_err("registry remove BEGIN", e))?;
tx.execute(
"SELECT pg_advisory_xact_lock($1)",
&[&lfs_objects_lock_key(schema)],
)
.await
.map_err(|e| io_err("registry remove lfs advisory lock", e))?;
tx.execute(
"SELECT pg_advisory_xact_lock($1)",
&[&git_refs_lock_key(schema)],
)
.await
.map_err(|e| io_err("registry remove git_refs advisory lock", e))?;
let seq: i64 = tx
.query_one(&bump, &[])
.await
.map_err(|e| io_err("registry remove seq bump", e))
.and_then(|row| seq_of(&row, "registry remove seq bump"))?;
let exists = tx
.query_opt(&exists_sql, &[&repo_id])
.await
.map_err(|e| io_err("registry remove existence read", e))?;
if exists.is_none() {
tx.rollback().await.ok();
return Err(RegistryError::NotFound);
}
tx.execute(&delete_record, &[&repo_id])
.await
.map_err(|e| io_err("registry remove record row", e))?;
tx.execute(&delete_refs, &[&repo_id])
.await
.map_err(|e| io_err("registry remove ref rows", e))?;
tx.execute(&delete_markers, &[&repo_id])
.await
.map_err(|e| io_err("registry remove marker rows", e))?;
tx.execute(&delete_lfs, &[&repo_id])
.await
.map_err(|e| io_err("registry remove lfs rows", e))?;
tx.execute(&recount_lfs, &[])
.await
.map_err(|e| io_err("registry remove lfs recount", e))?;
tx.commit()
.await
.map_err(|e| io_err("registry remove commit", e))?;
Ok(seq)
}
async fn durable_watermark(
client: &tokio_postgres::Client,
schema: &str,
) -> Result<i64, RegistryError> {
let sql = format!(
"SELECT value FROM {} WHERE id = 1",
qualify(schema, "registry_seq")
);
client
.query_one(&sql, &[])
.await
.map_err(|e| io_err("registry watermark read", e))?
.try_get(0)
.map_err(|e| io_err("registry watermark read", e))
}
async fn durable_rows_since(
client: &tokio_postgres::Client,
schema: &str,
seq: i64,
) -> Result<Vec<RegistryRow>, RegistryError> {
let sql = format!(
"SELECT repo_id, visibility, grants, seq FROM {} WHERE seq > $1 ORDER BY seq",
qualify(schema, "git_repos")
);
let rows = client
.query(&sql, &[&seq])
.await
.map_err(|e| io_err("registry rows_since query", e))?;
let mut since = Vec::with_capacity(rows.len());
for row in &rows {
let repo_id: String = row
.try_get(0)
.map_err(|e| io_err("registry rows_since row", e))?;
let visibility: String = row
.try_get(1)
.map_err(|e| io_err("registry rows_since row", e))?;
let grants: String = row
.try_get(2)
.map_err(|e| io_err("registry rows_since row", e))?;
let row_seq: i64 = row
.try_get(3)
.map_err(|e| io_err("registry rows_since row", e))?;
since.push(row_to_record(&repo_id, &visibility, &grants, row_seq)?);
}
Ok(since)
}
async fn durable_record_mutation(
client: &mut tokio_postgres::Client,
schema: &str,
repo_id: &str,
) -> Result<i64, RegistryError> {
let bump = seq_bump_sql(schema);
let exists_sql = format!(
"SELECT true FROM {} WHERE repo_id = $1",
qualify(schema, "git_repos")
);
let upsert_sql = format!(
"INSERT INTO {} (repo_id, seq) VALUES ($1, $2) \
ON CONFLICT (repo_id) DO UPDATE SET seq = excluded.seq",
qualify(schema, "repo_mutation_markers")
);
let tx = client
.transaction()
.await
.map_err(|e| io_err("mutation marker BEGIN", e))?;
let seq: i64 = tx
.query_one(&bump, &[])
.await
.map_err(|e| io_err("mutation marker seq bump", e))
.and_then(|row| seq_of(&row, "mutation marker seq bump"))?;
let exists = tx
.query_opt(&exists_sql, &[&repo_id])
.await
.map_err(|e| io_err("mutation marker existence read", e))?;
if exists.is_none() {
tx.rollback().await.ok();
return Err(RegistryError::NotFound);
}
tx.execute(&upsert_sql, &[&repo_id, &seq])
.await
.map_err(|e| io_err("mutation marker row", e))?;
tx.commit()
.await
.map_err(|e| io_err("mutation marker commit", e))?;
Ok(seq)
}
#[derive(Clone, Debug)]
pub struct PgRegistryOptions {
pub schema: String,
pub engine: alkstore_postgres::PgOpts,
pub blobs: alkblobs::PgOpts,
}
impl Default for PgRegistryOptions {
fn default() -> Self {
PgRegistryOptions {
schema: DEFAULT_SCHEMA.to_string(),
engine: alkstore_postgres::PgOpts::default(),
blobs: alkblobs::PgOpts::default(),
}
}
}
pub struct PgRegistryAssembly {
store: Arc<dyn alkstore::Store>,
durable: Arc<PgRegistry>,
registry: Arc<dyn RegistryWrite>,
provider: Arc<CachedRegistryProvider>,
reactor: Arc<Reactor>,
backend: Arc<RegistryBackend>,
refs: Arc<crate::refs::VersionedRefTable<crate::refs::PgRefTable>>,
refs_version: Arc<std::sync::atomic::AtomicU64>,
#[cfg(feature = "lfs")]
lfs: Arc<crate::lfs::VersionedLfsRows<crate::lfs::PgLfsRows>>,
#[cfg(feature = "lfs")]
lfs_version: Arc<std::sync::atomic::AtomicU64>,
}
impl PgRegistryAssembly {
pub async fn open(config: &str, opts: PgRegistryOptions) -> Result<Self, OpenError> {
let handle = CoTenantPg::open(config, &opts.schema).await?;
let store: Arc<dyn alkstore::Store> = Arc::from(
alkstore_postgres::open(config, opts.engine)
.await
.map_err(|e| OpenError::Engine(format!("alkstore-postgres open: {e}")))?,
);
let refs_handle = handle.clone();
let durable: Arc<PgRegistry> = Arc::new(PgRegistry::new(handle));
let provider = Arc::new(CachedRegistryProvider::new());
let rows = durable
.rows_since(0)
.await
.map_err(|e| OpenError::Engine(format!("cached provider initial load: {e}")))?;
provider.install(rows);
let durable_side: Arc<dyn RegistryWrite> = durable.clone();
let reactor = Arc::new(Reactor::new(
store.clone(),
durable_side.clone(),
provider.clone(),
));
let registry: Arc<dyn RegistryWrite> = Arc::new(AnnouncingRegistry::new(
durable_side.clone(),
reactor.clone(),
));
let backend = Arc::new(RegistryBackend::with_cached_provider(
registry.clone(),
provider.clone(),
));
#[cfg(feature = "lfs")]
let lfs_version = Arc::new(std::sync::atomic::AtomicU64::new(0));
#[cfg(feature = "lfs")]
let lfs = Arc::new(crate::lfs::VersionedLfsRows::new(
Arc::new(crate::lfs::PgLfsRows::new(refs_handle.clone())),
lfs_version.clone(),
));
let refs_version = Arc::new(std::sync::atomic::AtomicU64::new(0));
let refs = Arc::new(crate::refs::VersionedRefTable::new(
Arc::new(crate::refs::PgRefTable::new(refs_handle)),
refs_version.clone(),
));
Ok(PgRegistryAssembly {
store,
durable,
registry,
provider,
reactor,
backend,
refs,
refs_version,
#[cfg(feature = "lfs")]
lfs,
#[cfg(feature = "lfs")]
lfs_version,
})
}
pub fn store(&self) -> &Arc<dyn alkstore::Store> {
&self.store
}
pub fn write(&self) -> &Arc<dyn RegistryWrite> {
&self.registry
}
pub fn durable(&self) -> &Arc<PgRegistry> {
&self.durable
}
pub fn provider(&self) -> &Arc<CachedRegistryProvider> {
&self.provider
}
pub fn reactor(&self) -> &Arc<Reactor> {
&self.reactor
}
pub fn backend(&self) -> &Arc<RegistryBackend> {
&self.backend
}
pub fn refs(&self) -> &Arc<crate::refs::VersionedRefTable<crate::refs::PgRefTable>> {
&self.refs
}
pub fn refs_version(&self) -> &Arc<std::sync::atomic::AtomicU64> {
&self.refs_version
}
#[cfg(feature = "lfs")]
pub fn lfs_rows(&self) -> &Arc<crate::lfs::VersionedLfsRows<crate::lfs::PgLfsRows>> {
&self.lfs
}
#[cfg(feature = "lfs")]
pub fn lfs_version(&self) -> &Arc<std::sync::atomic::AtomicU64> {
&self.lfs_version
}
}
#[async_trait]
impl crate::pins::RegistryMutations for PgRegistryAssembly {
async fn record_mutation(&self, repo_id: &str) -> Result<i64, RegistryError> {
let seq = self.durable.record_mutation(repo_id).await?;
self.reactor
.announce(&RegistryEvent::Mutation {
seq,
repo_id: repo_id.to_owned(),
})
.await;
Ok(seq)
}
}
#[cfg(all(test, feature = "postgres"))]
pub(crate) mod live_harness {
use alkstore_postgres::quote_identifier;
use tokio_postgres::NoTls;
pub(crate) fn harness_dsn() -> Option<String> {
let pick = |alkgit_key: &str, alkblobs_key: &str, default: &str| {
std::env::var(alkgit_key)
.ok()
.filter(|v| !v.is_empty())
.or_else(|| std::env::var(alkblobs_key).ok().filter(|v| !v.is_empty()))
.unwrap_or_else(|| default.to_owned())
};
let host = std::env::var("ALKGIT_SUBSTRATE_PG_HOST")
.ok()
.filter(|v| !v.is_empty())
.or_else(|| {
std::env::var("ALKBLOBS_PG_HOST")
.ok()
.filter(|v| !v.is_empty())
})?;
let port = pick("ALKGIT_SUBSTRATE_PG_PORT", "ALKBLOBS_PG_PORT", "15432");
let user = pick("ALKGIT_SUBSTRATE_PG_USER", "ALKBLOBS_PG_USER", "postgres");
let password = pick(
"ALKGIT_SUBSTRATE_PG_PASSWORD",
"ALKBLOBS_PG_PASSWORD",
"poc",
);
let db = pick("ALKGIT_SUBSTRATE_PG_DB", "ALKBLOBS_PG_DB", "blobs");
Some(format!(
"host={host} port={port} user={user} password={password} dbname={db}"
))
}
macro_rules! live_pg {
() => {
match $crate::postgres::live_harness::harness_dsn() {
Some(dsn) => dsn,
None => {
eprintln!(
"skipping (no postgres harness server): set \
ALKGIT_SUBSTRATE_PG_HOST (or the ALKBLOBS_PG_* \
harness env)"
);
return;
}
}
};
}
pub(crate) use live_pg;
pub(crate) fn unique_schema(tag: &str) -> String {
static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
format!("alkgit_substrate_pg_{}_{}_{}", std::process::id(), n, tag)
}
pub(crate) async fn drop_schema(dsn: &str, schema: &str) {
if let Ok((client, conn)) = tokio_postgres::connect(dsn, NoTls).await {
tokio::spawn(async move {
let _ = conn.await;
});
let _ = client
.batch_execute(&format!(
"DROP SCHEMA IF EXISTS {} CASCADE",
quote_identifier(schema)
))
.await;
}
}
pub(crate) async fn connect_direct(dsn: &str) -> tokio_postgres::Client {
let (client, conn) = tokio_postgres::connect(dsn, NoTls).await.unwrap();
tokio::spawn(async move {
let _ = conn.await;
});
client
}
pub(crate) static LIVE_GATE: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use alkgit::AccessAction;
use crate::reactor::{AppliedWatermark, MAINTENANCE_QUEUE, WAKE_CHANNEL};
use crate::registry::RegistryRead;
use alkgit::{GitRegistry, GitRegistryStore};
use alkstore::{QueueOpts, StreamEvent};
use super::*;
use super::live_harness::{connect_direct, drop_schema, unique_schema, LIVE_GATE};
use crate::migrations::pg_twin::{build_steps, STAMP_TABLE, STEP_VERSIONS};
use crate::postgres::live_harness::live_pg;
use crate::testutil::{private_record, record, spawn_wake_runner, stop_and_join, wait_until};
fn assembly_opts(schema: &str) -> PgRegistryOptions {
PgRegistryOptions {
schema: schema.to_owned(),
engine: alkstore_postgres::PgOpts {
schema: format!("{schema}_engine"),
..alkstore_postgres::PgOpts::default()
},
..PgRegistryOptions::default()
}
}
async fn count(
client: &tokio_postgres::Client,
schema: &str,
table: &str,
repo_id: &str,
) -> i64 {
client
.query_one(
&format!(
"SELECT count(*) FROM {} WHERE repo_id = $1",
qualify(schema, table)
),
&[&repo_id],
)
.await
.unwrap()
.get::<_, i64>(0)
}
#[test]
fn the_pg_step_set_is_monotone_across_the_twins() {
assert_eq!(
STEP_VERSIONS,
&[1][..],
"step order monotone across the twins — \
sqlite's set carries the same integers"
);
let steps = build_steps("alkgit");
assert_eq!(
steps.iter().map(|s| s.version).collect::<Vec<_>>(),
STEP_VERSIONS.to_vec(),
"the built step set carries the pinned versions"
);
assert!(
steps
.iter()
.enumerate()
.all(|(i, s)| s.version == i as i64 + 1),
"the step set is contiguous from 1"
);
#[cfg(feature = "sqlite")]
assert_eq!(
crate::migrations::CoTenantSqlite::step_count(),
1,
"the sqlite twin's shipped set — the same ordered integers"
);
}
#[tokio::test]
async fn pg_twin_carries_the_family_write_semantics_and_an_exact_seq() {
let _gate = LIVE_GATE.lock().await;
let dsn = live_pg!();
let schema = unique_schema("family");
let handle = CoTenantPg::open(&dsn, &schema).await.unwrap();
assert_eq!(
handle.stamp().await.unwrap(),
1,
"a fresh schema — the initial step stamped"
);
assert_eq!(CoTenantPg::step_count(), 1);
let twin = PgRegistry::new(handle.clone());
let put_seq = twin.put(&record("owner/r")).await.unwrap();
assert_eq!(put_seq, 1, "first mutation carries seq 1");
let mut granted = private_record("owner/r");
granted
.grants
.insert("u-7731".to_owned(), vec![AccessAction::Write]);
let update_seq = twin.update(&granted).await.unwrap();
assert_eq!(update_seq, 2);
let rows = twin.rows_since(0).await.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].seq, update_seq);
assert_eq!(
rows[0].record, granted,
"the record round-trips the columns"
);
assert_eq!(twin.watermark().await.unwrap(), 2);
let remove_seq = twin.remove("owner/r").await.unwrap();
assert_eq!(remove_seq, 3);
assert_eq!(
twin.watermark().await.unwrap(),
3,
"the committed removal advances the watermark though it \
leaves no row"
);
assert!(twin.rows_since(0).await.unwrap().is_empty());
let second = connect_direct(&dsn).await;
let seq_now: i64 = second
.query_one(
&format!(
"SELECT value FROM {} WHERE id = 1",
qualify(&schema, "registry_seq")
),
&[],
)
.await
.unwrap()
.get(0);
assert_eq!(
seq_now, 3,
"the second connection sees the committed seq — the bump \
rode the row-write transaction"
);
drop(second);
let reopened = PgRegistry::new(CoTenantPg::open(&dsn, &schema).await.unwrap());
assert_eq!(
reopened.watermark().await.unwrap(),
3,
"the seq singleton is durable"
);
drop_schema(&dsn, &schema).await;
}
#[tokio::test]
async fn pg_twin_error_map_matches_the_family() {
let _gate = LIVE_GATE.lock().await;
let dsn = live_pg!();
let schema = unique_schema("errors");
let twin = PgRegistry::new(CoTenantPg::open(&dsn, &schema).await.unwrap());
assert_eq!(
twin.update(&record("ghost")).await.unwrap_err(),
RegistryError::NotFound
);
assert_eq!(
twin.remove("ghost").await.unwrap_err(),
RegistryError::NotFound
);
assert_eq!(
twin.record_mutation("ghost").await.unwrap_err(),
RegistryError::NotFound,
"a marker write is refused for a removed repo"
);
assert_eq!(twin.put(&record("owner/r")).await.unwrap(), 1);
assert_eq!(
twin.put(&record("owner/r")).await.unwrap_err(),
RegistryError::AlreadyExists
);
assert_eq!(
twin.watermark().await.unwrap(),
1,
"the failed duplicate put consumed no seq"
);
let malformed = record("Owner/name");
assert_eq!(
twin.put(&malformed).await.unwrap_err(),
RegistryError::Invalid
);
let mut empty_key = record("empty");
empty_key
.grants
.insert(String::new(), vec![AccessAction::Read]);
assert_eq!(
twin.put(&empty_key).await.unwrap_err(),
RegistryError::Invalid
);
assert_eq!(
twin.watermark().await.unwrap(),
1,
"the rejections consumed no seq"
);
drop_schema(&dsn, &schema).await;
}
#[tokio::test]
async fn put_seeds_the_unborn_head_and_remove_clears_the_repo_rows() {
let _gate = LIVE_GATE.lock().await;
let dsn = live_pg!();
let schema = unique_schema("seed-head");
let twin = PgRegistry::new(CoTenantPg::open(&dsn, &schema).await.unwrap());
twin.put(&record("owner/seeded")).await.unwrap();
let second = connect_direct(&dsn).await;
let head: Option<String> = second
.query_one(
&format!(
"SELECT symref_target FROM {} WHERE repo_id = $1 AND name = 'HEAD'",
qualify(&schema, "git_refs")
),
&[&"owner/seeded"],
)
.await
.unwrap()
.get(0);
assert_eq!(
head.as_deref(),
Some(HEAD_SEED_TARGET),
"put seeded the unborn symref row in the record's transaction"
);
twin.update(&private_record("owner/seeded")).await.unwrap();
let heads = count(&second, &schema, "git_refs", "owner/seeded").await;
assert_eq!(
heads, 1,
"the update's transaction did not touch the seeded row"
);
twin.record_mutation("owner/seeded").await.unwrap();
second
.execute(
&format!(
"INSERT INTO {} (repo_id, name, oid_hex, symref_target) \
VALUES ('owner/seeded', 'refs/heads/main', \
'0123456789abcdef0123456789abcdef01234567', NULL)",
qualify(&schema, "git_refs")
),
&[],
)
.await
.unwrap();
twin.put(&record("owner/survivor")).await.unwrap();
twin.remove("owner/seeded").await.unwrap();
for (label, table) in [
("the record row", "git_repos"),
("the ref rows (the HEAD seed included)", "git_refs"),
("the mutation-marker rows", "repo_mutation_markers"),
] {
assert_eq!(
count(&second, &schema, table, "owner/seeded").await,
0,
"{label} removed with the registry removal"
);
}
assert_eq!(
count(&second, &schema, "git_repos", "owner/survivor").await,
1,
"a sister repo's row survived the removal"
);
drop(second);
drop_schema(&dsn, &schema).await;
}
#[tokio::test]
async fn record_mutation_bumps_the_watermark_without_touching_the_record() {
let _gate = LIVE_GATE.lock().await;
let dsn = live_pg!();
let schema = unique_schema("marker");
let handle = CoTenantPg::open(&dsn, &schema).await.unwrap();
let twin = PgRegistry::new(handle.clone());
let put_seq = twin.put(&record("owner/m")).await.unwrap();
assert_eq!(put_seq, 1);
let seq = twin.record_mutation("owner/m").await.unwrap();
assert_eq!(seq, 2);
assert_eq!(twin.watermark().await.unwrap(), 2);
let rows = twin.rows_since(0).await.unwrap();
assert_eq!(
rows.iter().map(|row| row.seq).collect::<Vec<_>>(),
vec![put_seq],
"the marker left no record row — the record schema is \
untouched"
);
let marker: i64 = handle
.lock()
.await
.query_one(
&format!(
"SELECT seq FROM {} WHERE repo_id = $1",
qualify(&schema, "repo_mutation_markers")
),
&[&"owner/m"],
)
.await
.unwrap()
.get(0);
assert_eq!(marker, seq, "the marker row carries its seq");
let again = twin.record_mutation("owner/m").await.unwrap();
assert_eq!(again, 3, "the marker row upserts, never duplicates");
twin.remove("owner/m").await.unwrap();
let second = connect_direct(&dsn).await;
assert_eq!(
count(&second, &schema, "repo_mutation_markers", "owner/m").await,
0,
"the removal took the repo's marker rows with it"
);
drop(second);
drop_schema(&dsn, &schema).await;
}
#[tokio::test]
async fn announced_store_binding_fires_the_composition_post_commit() {
let _gate = LIVE_GATE.lock().await;
let dsn = live_pg!();
let schema = unique_schema("announce");
let assembly = PgRegistryAssembly::open(&dsn, assembly_opts(&schema))
.await
.unwrap();
let mut listener = assembly.store().listen(WAKE_CHANNEL).await.unwrap();
let mut granted = record("owner/a");
granted
.grants
.insert("u-1".to_owned(), vec![AccessAction::Read]);
assembly.backend().put_repo(&granted).await.unwrap();
let events: Vec<StreamEvent> = assembly
.store()
.stream(crate::reactor::EVENTS_STREAM)
.await
.unwrap()
.read_since(0, 10)
.await
.unwrap();
assert_eq!(events.len(), 1, "one announced put = one feed event");
assert_eq!(events[0].key.as_deref(), Some("owner/a"));
assert_eq!(
crate::reactor::RegistryEvent::decode_event(&events[0]),
Some(crate::reactor::RegistryEvent::Put {
seq: 1,
record: granted.clone(),
})
);
let wake = listener
.recv_timeout(Duration::from_secs(2))
.await
.unwrap()
.expect("the announce's NOTIFY delivered over the LISTEN leg");
assert_eq!(wake.channel, WAKE_CHANNEL);
let queue = assembly
.store()
.queue(MAINTENANCE_QUEUE, QueueOpts::default())
.await
.unwrap();
let claimed = queue
.claim_one("probe")
.await
.unwrap()
.expect("the pack-housekeeping job");
assert_eq!(
claimed
.job()
.payload_as::<crate::reactor::MaintenanceJob>()
.unwrap(),
crate::reactor::MaintenanceJob::PackHousekeeping {
repo_id: "owner/a".to_owned(),
}
);
assert_eq!(
assembly.backend().resolve("owner/a").await.unwrap(),
granted,
"the write-through provider serves the mutation"
);
assembly.backend().remove_repo("owner/a").await.unwrap();
let events: Vec<StreamEvent> = assembly
.store()
.stream(crate::reactor::EVENTS_STREAM)
.await
.unwrap()
.read_since(1, 10)
.await
.unwrap();
assert_eq!(
events
.first()
.and_then(crate::reactor::RegistryEvent::decode_event),
Some(crate::reactor::RegistryEvent::Removed {
seq: 2,
repo_id: "owner/a".to_owned(),
})
);
drop(listener);
drop_schema(&dsn, &schema).await;
drop_schema(&dsn, &format!("{schema}_engine")).await;
}
#[tokio::test]
async fn remove_repo_takes_the_lfs_rows_and_recounts_the_survivors() {
let dsn = live_pg!();
let schema = unique_schema("remove-lfs");
let assembly = PgRegistryAssembly::open(&dsn, assembly_opts(&schema))
.await
.unwrap();
assembly.durable().put(&record("victim")).await.unwrap();
assembly.durable().put(&record("survivor")).await.unwrap();
let shared_oid = "01".repeat(32);
let victim_only_oid = "02".repeat(32);
{
let guard = assembly.durable().handle.lock().await;
guard
.execute(
&format!(
"INSERT INTO {} (repo_id, oid, size, refcount) \
VALUES ('victim', $1, 8, 2), ('survivor', $1, 8, 2), \
('victim', $2, 4, 1)",
qualify(&schema, "lfs_objects")
),
&[&shared_oid, &victim_only_oid],
)
.await
.unwrap();
}
assembly.backend().remove_repo("victim").await.unwrap();
let second = connect_direct(&dsn).await;
let victim_only: i64 = second
.query_one(
&format!(
"SELECT count(*) FROM {} WHERE oid = $1",
qualify(&schema, "lfs_objects")
),
&[&victim_only_oid],
)
.await
.unwrap()
.get(0);
assert_eq!(victim_only, 0, "the victim's own oid left no row at all");
let shared_count: i64 = second
.query_one(
&format!(
"SELECT refcount FROM {} WHERE repo_id = 'survivor' AND oid = $1",
qualify(&schema, "lfs_objects")
),
&[&shared_oid],
)
.await
.unwrap()
.get(0);
assert_eq!(
shared_count, 1,
"the surviving row carries the recomputed count (the re-protection contract's data)"
);
drop(second);
#[cfg(feature = "lfs")]
assert_eq!(
crate::lfs::LfsRows::repo_rows(assembly.lfs_rows().as_ref(), "survivor")
.await
.unwrap(),
vec![crate::lfs::LfsRow {
repo_id: "survivor".to_owned(),
oid: shared_oid,
size: 8,
refcount: 1,
}],
"the surface reads back the same state (the composition's row store)"
);
drop_schema(&dsn, &schema).await;
drop_schema(&dsn, &format!("{schema}_engine")).await;
}
#[tokio::test]
async fn the_asymmetry_probe_notify_free_write_wakes_nothing_and_reconcile_closes_the_gap() {
let _gate = LIVE_GATE.lock().await;
let dsn = live_pg!();
let schema = unique_schema("asymmetry");
let assembly = PgRegistryAssembly::open(&dsn, assembly_opts(&schema))
.await
.unwrap();
let mut listener = assembly.store().listen(WAKE_CHANNEL).await.unwrap();
let (stop, runner) = spawn_wake_runner(assembly.reactor().clone());
tokio::time::sleep(Duration::from_millis(200)).await;
let seq = assembly
.durable()
.put(&private_record("owner/silent"))
.await
.unwrap();
let wake = listener
.recv_timeout(Duration::from_millis(700))
.await
.unwrap();
assert!(
wake.is_none(),
"a notify-free write wakes nothing on postgres — no \
file-level bump exists (LISTEN/NOTIFY is per-notify)"
);
assert_eq!(
assembly.provider().resolve("owner/silent"),
None,
"the cache is stale — the reconcile tick's cadence is the \
staleness bound"
);
let applied = AppliedWatermark::new();
let caught = assembly.reactor().reconcile(&applied).await.unwrap();
assert_eq!(
caught
.iter()
.map(|row| row.record.repo_id.as_str())
.collect::<Vec<_>>(),
vec!["owner/silent"],
"the reconcile sweep closed the gap in one pass"
);
assert_eq!(
assembly.provider().resolve("owner/silent"),
Some(private_record("owner/silent"))
);
assert_eq!(
assembly.durable().watermark().await.unwrap(),
seq,
"the watermark advanced though nothing announced"
);
assembly
.write()
.update(&record("owner/silent"))
.await
.unwrap();
let wake = listener
.recv_timeout(Duration::from_secs(2))
.await
.unwrap()
.expect("the announced write's NOTIFY delivered");
assert_eq!(wake.channel, WAKE_CHANNEL);
wait_until(|| {
assembly
.provider()
.resolve("owner/silent")
.as_ref()
.is_some_and(|resolved| resolved == &record("owner/silent"))
})
.await;
drop(listener);
stop_and_join(stop, runner).await;
drop_schema(&dsn, &schema).await;
drop_schema(&dsn, &format!("{schema}_engine")).await;
}
#[tokio::test]
async fn assembly_open_is_loud_and_loads_the_persisted_registry() {
let _gate = LIVE_GATE.lock().await;
let dsn = live_pg!();
let schema = unique_schema("reopen");
{
let assembly = PgRegistryAssembly::open(&dsn, assembly_opts(&schema))
.await
.unwrap();
assembly
.backend()
.put_repo(&private_record("owner/kept"))
.await
.unwrap();
assert_eq!(assembly.durable().watermark().await.unwrap(), 1);
}
let reopened = PgRegistryAssembly::open(&dsn, assembly_opts(&schema))
.await
.unwrap();
assert_eq!(
reopened.backend().resolve("owner/kept").await.unwrap(),
private_record("owner/kept"),
"the initial snapshot load carried the persisted table"
);
assert_eq!(
reopened.durable().watermark().await.unwrap(),
1,
"the seq singleton persisted and continues the mutation count"
);
assert_eq!(
reopened.provider().refresh_count(),
1,
"load only — no refreshes at construction beyond the \
initial snapshot"
);
drop_schema(&dsn, &schema).await;
drop_schema(&dsn, &format!("{schema}_engine")).await;
}
#[tokio::test]
async fn corrupt_record_rows_map_registry_invalid() {
let _gate = LIVE_GATE.lock().await;
let dsn = live_pg!();
let schema = unique_schema("corrupt");
let handle = CoTenantPg::open(&dsn, &schema).await.unwrap();
let twin = PgRegistry::new(handle.clone());
twin.put(&record("owner/ok")).await.unwrap();
let second = connect_direct(&dsn).await;
second
.execute(
&format!(
"INSERT INTO {} (repo_id, visibility, grants, seq) \
VALUES ('owner/bent', 'public', 'not-json', 2)",
qualify(&schema, "git_repos")
),
&[],
)
.await
.unwrap();
drop(second);
let err = twin.rows_since(0).await.unwrap_err();
assert_eq!(err, RegistryError::Invalid, "{err:?}");
drop_schema(&dsn, &schema).await;
}
#[tokio::test]
async fn concurrent_open_of_one_schema_races_to_one_stamped_schema() {
let _gate = LIVE_GATE.lock().await;
let dsn = live_pg!();
let schema = unique_schema("concurrent");
let (a, b) = tokio::join!(
CoTenantPg::open(&dsn, &schema),
CoTenantPg::open(&dsn, &schema)
);
let a = a.expect("no opener fails");
let b = b.expect("no opener fails");
assert_eq!(a.stamp().await.unwrap(), 1);
assert_eq!(
b.stamp().await.unwrap(),
1,
"the benign race is swallowed — both openers converge on \
the stamped schema"
);
drop_schema(&dsn, &schema).await;
}
#[tokio::test]
async fn an_unknown_forward_stamp_is_safe_to_open_against() {
let _gate = LIVE_GATE.lock().await;
let dsn = live_pg!();
let schema = unique_schema("forward-stamp");
{
let handle = CoTenantPg::open(&dsn, &schema).await.unwrap();
assert_eq!(handle.stamp().await.unwrap(), 1, "the initial step stamped");
let second = connect_direct(&dsn).await;
second
.execute(
&format!(
"UPDATE {} SET stamp = 99 WHERE id = 1",
qualify(&schema, STAMP_TABLE)
),
&[],
)
.await
.unwrap();
drop(second);
}
let reopened = CoTenantPg::open(&dsn, &schema).await.unwrap();
assert_eq!(
reopened.stamp().await.unwrap(),
99,
"the forward stamp is left untouched"
);
let second = connect_direct(&dsn).await;
let rows: i64 = second
.query_one(
&format!("SELECT count(*) FROM {}", qualify(&schema, "git_repos")),
&[],
)
.await
.unwrap()
.get(0);
assert_eq!(rows, 0, "the schema is intact — the tables still serve");
drop(second);
drop_schema(&dsn, &schema).await;
}
#[tokio::test]
async fn open_is_loud_on_unreachable_and_unusable_input() {
assert!(
matches!(
CoTenantPg::open(
"host=127.0.0.1 port=1 user=postgres password=x dbname=x",
""
)
.await,
Err(OpenError::Engine(_))
),
"an unreachable server fails loud at construction"
);
assert!(
matches!(
CoTenantPg::open("", "\"bent\"").await,
Err(OpenError::Invalid(_))
),
"the schema-name boundary rejects before any connection"
);
}
}