Skip to main content

flint_store/
layout.rs

1//! Where each flint product keeps the cell that says "I am the writer
2//! of this prefix" — and the probe that finds a NEIGHBOUR's.
3//!
4//! WHY THIS TABLE IS SHARED. One prefix belongs to exactly one product.
5//! Within a product that is a mechanism: every writer contends for the
6//! product's one epoch cell — forge admits a single writer, lean's
7//! writers take turns at it — and the loser waits. ACROSS products it
8//! is a convention, because each product derives its own cell key and
9//! the keys are disjoint — forge's `git/epoch`, lean's
10//! `.flint/lean/epoch`. Point a forge repository and a lean workspace
11//! at one prefix and both acquire, both are correct that they hold
12//! THEIR cell, and neither can see the other. There is no 412, no
13//! fence and no log line (composition drill C1).
14//!
15//! Nothing here changes that. Prevention is left to whatever assigns
16//! prefixes — an admission policy, a GitOps path, a platform module —
17//! and this module exists so that when that prevention fails, the
18//! failure is NOISY instead of silent. That is the whole contract: a
19//! control you cannot audit is one you find out about from a support
20//! ticket.
21//!
22//! WHY A HEAD AND NOT A LISTING. The first sketch listed the prefix and
23//! looked for a foreign control DIRECTORY. An exact probe of the
24//! neighbour's epoch cell is strictly better: one request instead of a
25//! paginated listing, it detects a WRITER rather than the litter a
26//! writer leaves, and it cannot be confused by nesting — forge's own
27//! export at `<prefix>/inner` puts lean's cell at
28//! `<prefix>/inner/.flint/lean/epoch`, which is not the key forge
29//! probes.
30//!
31//! ADDING A PRODUCT means adding a row here and calling `neighbours`
32//! with your own kind. Do not re-derive a neighbour's key at a call
33//! site: a key that drifts turns this check into a permanent all-clear,
34//! which is worse than not having it.
35
36use super::{ObjectStore, StoreResult};
37
38/// A flint writer kind, identified by the cell it arbitrates on.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum Writer {
41    /// `flint-forge-syncer` over a bare git repository.
42    ForgeRepository,
43    /// A `flint-sync` syncer over a lean workspace. Forge's legible
44    /// export is one of these, published by forge rather than by an
45    /// agent's syncer.
46    LeanWorkspace,
47}
48
49impl Writer {
50    /// This kind's epoch cell under `prefix` (no trailing slash).
51    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    /// The knob an operator would reach for to correct it.
66    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/// What a probe found: somebody else's lease cell under our prefix.
77#[derive(Debug, Clone)]
78pub struct Foreign {
79    pub kind: Writer,
80    pub key: String,
81    pub holder_id: String,
82    pub epoch: u64,
83    /// The holder shut down cleanly. The prefix is still shared — the
84    /// workspace's objects are all still there — but nothing is writing
85    /// it at this instant.
86    pub released: bool,
87    /// The STORE's clock for the last renewal, never ours (A8).
88    pub last_renew_unix: Option<u64>,
89}
90
91impl Foreign {
92    /// One line, written for whoever is reading logs at 3am: what is
93    /// there, whether it is live, and which field to change.
94    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
121/// Probe `prefix` for the lease cells of every writer kind that is not
122/// `mine`. One HEAD-shaped read per foreign kind (currently one).
123///
124/// A store error is NOT a finding and NOT a failure: this is a
125/// diagnostic, and a prefix that cannot be probed must never stop a
126/// writer that would otherwise have started. Errors are swallowed here
127/// so that no caller can accidentally make this fatal.
128pub 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    /// The condition drill C1 measured: two products, one prefix, both
160    /// holding, neither able to see the other. Each must now find the
161    /// other's cell.
162    #[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    /// A writer must never report ITSELF. This is the leg that would
180    /// turn the check into a permanent false alarm on every healthy
181    /// deployment, which is how a diagnostic gets switched off.
182    #[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    /// The probe is an EXACT key, and that is what makes forge's own
194    /// legible export invisible to it.
195    ///
196    /// A repository at `t/a` may export to `t/a/inner` — the syncer's
197    /// self-collision guard is string equality, so nesting is admitted
198    /// (drill C1b). That export is a real lean workspace and writes a
199    /// real lean epoch cell. If this probe listed the subtree instead
200    /// of naming a key, every such repository would report itself as
201    /// colliding with its own export, forever.
202    #[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    /// A released cell still means the prefix is shared: the objects
214    /// are there and the holder takes it back on restart. It is
215    /// reported, and the wording says which it is.
216    #[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    /// The report has to be usable by whoever reads it, so it names
232    /// the neighbour, the cell, and the field to change.
233    #[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}