use super::{ObjectStore, StoreResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Writer {
ForgeRepository,
LeanWorkspace,
}
impl Writer {
pub fn epoch_key(&self, prefix: &str) -> String {
match self {
Writer::ForgeRepository => format!("{prefix}/git/epoch"),
Writer::LeanWorkspace => format!("{prefix}/.flint/lean/epoch"),
}
}
pub fn label(&self) -> &'static str {
match self {
Writer::ForgeRepository => "a forge repository",
Writer::LeanWorkspace => "a lean workspace",
}
}
pub fn prefix_field(&self) -> &'static str {
match self {
Writer::ForgeRepository => "FlintRepo.spec.keyPrefix",
Writer::LeanWorkspace => "the lean workspace prefix",
}
}
pub const ALL: [Writer; 2] = [Writer::ForgeRepository, Writer::LeanWorkspace];
}
#[derive(Debug, Clone)]
pub struct Foreign {
pub kind: Writer,
pub key: String,
pub holder_id: String,
pub epoch: u64,
pub released: bool,
pub last_renew_unix: Option<u64>,
}
impl Foreign {
pub fn report(&self) -> String {
let liveness = if self.released {
"it released cleanly, so nothing is writing it right now — but its objects are \
still there and it will take the prefix back when it restarts"
.to_string()
} else {
match self.last_renew_unix {
Some(t) => format!("it is HOLDING that lease (last renewed at {t} by the store's clock)"),
None => "it is holding that lease".to_string(),
}
};
format!(
"PREFIX SHARED WITH ANOTHER PRODUCT: {} also writes this prefix. Its lease cell is \
{} (holder {}, epoch {}), and {}. One prefix belongs to exactly one product, and this \
is NOT enforced across products — the two arbitrate on different cells and cannot see \
each other, so neither will ever fence the other and the loser's writes are lost \
quietly. Give one of them its own prefix ({} names it).",
self.kind.label(),
self.key,
self.holder_id,
self.epoch,
liveness,
self.kind.prefix_field(),
)
}
}
pub async fn neighbours(
store: &dyn ObjectStore,
prefix: &str,
mine: Writer,
) -> StoreResult<Vec<Foreign>> {
let mut found = vec![];
for kind in Writer::ALL.iter().copied().filter(|k| *k != mine) {
let key = kind.epoch_key(prefix);
if let Ok(Some(state)) = store.epoch_read(&key).await {
found.push(Foreign {
kind,
key,
holder_id: state.holder_id,
epoch: state.epoch,
released: state.released,
last_renew_unix: state.last_renew_unix,
});
}
}
Ok(found)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory::MemoryStore;
async fn claim(store: &MemoryStore, key: &str, holder: &str) {
store.epoch_acquire(key, holder, None).await.expect("acquire");
}
#[tokio::test]
async fn each_product_finds_the_others_lease_under_one_prefix() {
let store = MemoryStore::new();
claim(&store, &Writer::ForgeRepository.epoch_key("t/shared"), "forge-1").await;
claim(&store, &Writer::LeanWorkspace.epoch_key("t/shared"), "lean-1").await;
let seen = neighbours(&store, "t/shared", Writer::ForgeRepository).await.unwrap();
assert_eq!(seen.len(), 1, "forge must see the lean workspace");
assert_eq!(seen[0].kind, Writer::LeanWorkspace);
assert_eq!(seen[0].holder_id, "lean-1");
assert!(!seen[0].released);
let seen = neighbours(&store, "t/shared", Writer::LeanWorkspace).await.unwrap();
assert_eq!(seen.len(), 1, "lean must see the forge repository");
assert_eq!(seen[0].holder_id, "forge-1");
}
#[tokio::test]
async fn a_writer_alone_on_its_prefix_finds_nothing() {
let store = MemoryStore::new();
claim(&store, &Writer::ForgeRepository.epoch_key("t/solo"), "forge-1").await;
assert!(neighbours(&store, "t/solo", Writer::ForgeRepository).await.unwrap().is_empty());
let store2 = MemoryStore::new();
claim(&store2, &Writer::LeanWorkspace.epoch_key("t/solo"), "lean-1").await;
assert!(neighbours(&store2, "t/solo", Writer::LeanWorkspace).await.unwrap().is_empty());
}
#[tokio::test]
async fn a_nested_export_is_not_mistaken_for_a_foreign_writer() {
let store = MemoryStore::new();
claim(&store, &Writer::ForgeRepository.epoch_key("t/a"), "forge-1").await;
claim(&store, &Writer::LeanWorkspace.epoch_key("t/a/inner"), "forge-1-export").await;
assert!(
neighbours(&store, "t/a", Writer::ForgeRepository).await.unwrap().is_empty(),
"a repository must not report its own nested export"
);
}
#[tokio::test]
async fn a_released_neighbour_is_still_reported_and_says_so() {
let store = MemoryStore::new();
let key = Writer::LeanWorkspace.epoch_key("t/quiet");
let lease = store.epoch_acquire(&key, "lean-1", None).await.unwrap();
store.epoch_release(&key, &lease).await.unwrap();
let seen = neighbours(&store, "t/quiet", Writer::ForgeRepository).await.unwrap();
assert_eq!(seen.len(), 1, "a clean release does not make the prefix unshared");
assert!(seen[0].released);
let r = seen[0].report();
assert!(r.contains("released cleanly"), "{r}");
assert!(!r.contains("HOLDING"), "a released cell must not read as live: {r}");
}
#[tokio::test]
async fn the_report_names_the_cell_and_the_field_to_change() {
let store = MemoryStore::new();
claim(&store, &Writer::LeanWorkspace.epoch_key("t/x"), "lean-9").await;
let seen = neighbours(&store, "t/x", Writer::ForgeRepository).await.unwrap();
let r = seen[0].report();
assert!(r.contains("t/x/.flint/lean/epoch"), "{r}");
assert!(r.contains("lean-9"), "{r}");
assert!(r.contains("a lean workspace"), "{r}");
assert!(r.contains("NOT enforced across products"), "{r}");
}
}