1use super::{ObjectStore, StoreResult};
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum Writer {
41 ForgeRepository,
43 LeanWorkspace,
47}
48
49impl Writer {
50 pub fn epoch_key(&self, prefix: &str) -> String {
52 match self {
53 Writer::ForgeRepository => format!("{prefix}/git/epoch"),
54 Writer::LeanWorkspace => format!("{prefix}/.flint/lean/epoch"),
55 }
56 }
57
58 pub fn label(&self) -> &'static str {
59 match self {
60 Writer::ForgeRepository => "a forge repository",
61 Writer::LeanWorkspace => "a lean workspace",
62 }
63 }
64
65 pub fn prefix_field(&self) -> &'static str {
67 match self {
68 Writer::ForgeRepository => "FlintRepo.spec.keyPrefix",
69 Writer::LeanWorkspace => "the lean workspace prefix",
70 }
71 }
72
73 pub const ALL: [Writer; 2] = [Writer::ForgeRepository, Writer::LeanWorkspace];
74}
75
76#[derive(Debug, Clone)]
78pub struct Foreign {
79 pub kind: Writer,
80 pub key: String,
81 pub holder_id: String,
82 pub epoch: u64,
83 pub released: bool,
87 pub last_renew_unix: Option<u64>,
89}
90
91impl Foreign {
92 pub fn report(&self) -> String {
95 let liveness = if self.released {
96 "it released cleanly, so nothing is writing it right now — but its objects are \
97 still there and it will take the prefix back when it restarts"
98 .to_string()
99 } else {
100 match self.last_renew_unix {
101 Some(t) => format!("it is HOLDING that lease (last renewed at {t} by the store's clock)"),
102 None => "it is holding that lease".to_string(),
103 }
104 };
105 format!(
106 "PREFIX SHARED WITH ANOTHER PRODUCT: {} also writes this prefix. Its lease cell is \
107 {} (holder {}, epoch {}), and {}. One prefix belongs to exactly one product, and this \
108 is NOT enforced across products — the two arbitrate on different cells and cannot see \
109 each other, so neither will ever fence the other and the loser's writes are lost \
110 quietly. Give one of them its own prefix ({} names it).",
111 self.kind.label(),
112 self.key,
113 self.holder_id,
114 self.epoch,
115 liveness,
116 self.kind.prefix_field(),
117 )
118 }
119}
120
121pub async fn neighbours(
129 store: &dyn ObjectStore,
130 prefix: &str,
131 mine: Writer,
132) -> StoreResult<Vec<Foreign>> {
133 let mut found = vec![];
134 for kind in Writer::ALL.iter().copied().filter(|k| *k != mine) {
135 let key = kind.epoch_key(prefix);
136 if let Ok(Some(state)) = store.epoch_read(&key).await {
137 found.push(Foreign {
138 kind,
139 key,
140 holder_id: state.holder_id,
141 epoch: state.epoch,
142 released: state.released,
143 last_renew_unix: state.last_renew_unix,
144 });
145 }
146 }
147 Ok(found)
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::memory::MemoryStore;
154
155 async fn claim(store: &MemoryStore, key: &str, holder: &str) {
156 store.epoch_acquire(key, holder, None).await.expect("acquire");
157 }
158
159 #[tokio::test]
163 async fn each_product_finds_the_others_lease_under_one_prefix() {
164 let store = MemoryStore::new();
165 claim(&store, &Writer::ForgeRepository.epoch_key("t/shared"), "forge-1").await;
166 claim(&store, &Writer::LeanWorkspace.epoch_key("t/shared"), "lean-1").await;
167
168 let seen = neighbours(&store, "t/shared", Writer::ForgeRepository).await.unwrap();
169 assert_eq!(seen.len(), 1, "forge must see the lean workspace");
170 assert_eq!(seen[0].kind, Writer::LeanWorkspace);
171 assert_eq!(seen[0].holder_id, "lean-1");
172 assert!(!seen[0].released);
173
174 let seen = neighbours(&store, "t/shared", Writer::LeanWorkspace).await.unwrap();
175 assert_eq!(seen.len(), 1, "lean must see the forge repository");
176 assert_eq!(seen[0].holder_id, "forge-1");
177 }
178
179 #[tokio::test]
183 async fn a_writer_alone_on_its_prefix_finds_nothing() {
184 let store = MemoryStore::new();
185 claim(&store, &Writer::ForgeRepository.epoch_key("t/solo"), "forge-1").await;
186 assert!(neighbours(&store, "t/solo", Writer::ForgeRepository).await.unwrap().is_empty());
187
188 let store2 = MemoryStore::new();
189 claim(&store2, &Writer::LeanWorkspace.epoch_key("t/solo"), "lean-1").await;
190 assert!(neighbours(&store2, "t/solo", Writer::LeanWorkspace).await.unwrap().is_empty());
191 }
192
193 #[tokio::test]
203 async fn a_nested_export_is_not_mistaken_for_a_foreign_writer() {
204 let store = MemoryStore::new();
205 claim(&store, &Writer::ForgeRepository.epoch_key("t/a"), "forge-1").await;
206 claim(&store, &Writer::LeanWorkspace.epoch_key("t/a/inner"), "forge-1-export").await;
207 assert!(
208 neighbours(&store, "t/a", Writer::ForgeRepository).await.unwrap().is_empty(),
209 "a repository must not report its own nested export"
210 );
211 }
212
213 #[tokio::test]
217 async fn a_released_neighbour_is_still_reported_and_says_so() {
218 let store = MemoryStore::new();
219 let key = Writer::LeanWorkspace.epoch_key("t/quiet");
220 let lease = store.epoch_acquire(&key, "lean-1", None).await.unwrap();
221 store.epoch_release(&key, &lease).await.unwrap();
222
223 let seen = neighbours(&store, "t/quiet", Writer::ForgeRepository).await.unwrap();
224 assert_eq!(seen.len(), 1, "a clean release does not make the prefix unshared");
225 assert!(seen[0].released);
226 let r = seen[0].report();
227 assert!(r.contains("released cleanly"), "{r}");
228 assert!(!r.contains("HOLDING"), "a released cell must not read as live: {r}");
229 }
230
231 #[tokio::test]
234 async fn the_report_names_the_cell_and_the_field_to_change() {
235 let store = MemoryStore::new();
236 claim(&store, &Writer::LeanWorkspace.epoch_key("t/x"), "lean-9").await;
237 let seen = neighbours(&store, "t/x", Writer::ForgeRepository).await.unwrap();
238 let r = seen[0].report();
239 assert!(r.contains("t/x/.flint/lean/epoch"), "{r}");
240 assert!(r.contains("lean-9"), "{r}");
241 assert!(r.contains("a lean workspace"), "{r}");
242 assert!(r.contains("NOT enforced across products"), "{r}");
243 }
244}