Skip to main content

energyml_cache/
index.rs

1//! The package index of a store, built when first asked for and kept exact by
2//! the store's own revision (ADR-0010),
3//! moved here from a WebAssembly front end.
4//!
5//! # What is kept
6//!
7//! Nothing but the [`IndexSlot`]: the index itself, and a builder while a pass
8//! is in progress. No object is held here.
9//!
10//! # The invalidation rule (ADR-0010)
11//!
12//! **The store says what changed; a face's own journal plays no part.**
13//! [`IndexSlot::with`] asks `store.changed_since(index.revision())` on every
14//! question: `Nothing` keeps the index, `Some(uuids)` re-reads those objects
15//! (`PackageIndex::update`), `Everything` rebuilds. Ctrl+Z, `create_apply`,
16//! `repair_references` and an ETP `put_xml` all write through the overlay, so
17//! all of them are covered without this file knowing they exist.
18//!
19//! The slot is a set of `RefCell`s: a face's methods take `&self` and wasm is
20//! single-threaded. [`Cache`](crate::Cache) is what decides whether a slot is
21//! used at all.
22
23use std::cell::{Cell, RefCell};
24
25use energyml_domain::package_index::{IndexBuilder, PackageIndex, Progress};
26use energyml_storage::EpcStore;
27use energyml_storage::epc::revision::Changed;
28
29/// The package index of a store, built when first asked for and kept exact by
30/// the store's own revision.
31///
32/// No borrow is held across a call that could come back in: a
33/// question asked **while** another is being answered (a reader that calls
34/// another reader) finds the index it is already holding and uses it.
35#[derive(Default)]
36pub struct IndexSlot {
37    index: RefCell<Option<PackageIndex>>,
38    /// A pass begun by [`IndexSlot::step`] and not finished: the slices of an incremental pass.
39    builder: RefCell<Option<IndexBuilder>>,
40    /// See [`IndexSlot::set_partial`].
41    partial: Cell<bool>,
42    /// What the index's updates have disturbed since each consumer last asked
43    /// ([`IndexSlot::take_disturbed`]) - what a cache of derived answers must
44    /// drop, see `PackageIndex::update_with`. **One account per consumer**: the
45    /// colour walks and the attachment listings each keep answers derived from
46    /// the same links, and the one that asked first must not eat what the other
47    /// still has to drop.
48    disturbed: [RefCell<Disturbed>; 2],
49}
50
51/// A cache of answers derived from the index's links: it has its own account of
52/// what the updates disturbed.
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub enum Consumer {
55    /// [`crate::colours`]: the colour walks.
56    Colours,
57    /// [`crate::Cache::attachments_of`]: the listings of what is attached to an object.
58    Attachments,
59}
60
61/// An account that nobody reads grows with every edit; past this many uuids it
62/// is told to throw everything away instead - too wide costs a re-walk, never
63/// a stale answer.
64const DISTURBED_CAP: usize = 50_000;
65
66/// What the index changed under a cache of what is derived from it.
67#[derive(Default, Clone, Debug, PartialEq, Eq)]
68pub struct Disturbed {
69    /// The index was built (or rebuilt) from scratch: nothing derived from the
70    /// previous one is known to hold.
71    pub everything: bool,
72    /// Lowercase uuids whose derived answers may have changed - the objects that
73    /// changed and the ones that gained or lost a reference from them.
74    pub uuids: Vec<String>,
75}
76
77/// What [`IndexSlot::settle`] decided.
78enum Plan {
79    Keep,
80    Build,
81    Update(Vec<String>),
82}
83
84impl IndexSlot {
85    /// Say that the store is **not the whole package**: an ETP connection holds
86    /// what was fetched and nothing else.
87    ///
88    /// The index is then marked incomplete (`PackageIndex::is_complete` is
89    /// `false`: the index of a connection is never `complete`) -
90    /// it answers for **what was taken**, and a caller that needs more asks
91    /// the server. It does not change what is indexed: the store's inventory is
92    /// what it is. Sticky: it describes the store, so a rebuild keeps it, until
93    /// `set_partial(false)` is said.
94    pub fn set_partial(&self, partial: bool) {
95        self.partial.set(partial);
96        if partial
97            && let Ok(mut slot) = self.index.try_borrow_mut()
98            && let Some(index) = slot.as_mut()
99        {
100            index.mark_incomplete();
101        }
102    }
103
104    /// Drop the index and any pass in progress; the next question rebuilds.
105    pub fn forget(&self) {
106        if let Ok(mut slot) = self.index.try_borrow_mut() {
107            *slot = None;
108        }
109        self.note(|dirty| dirty.everything = true);
110        if let Ok(mut building) = self.builder.try_borrow_mut() {
111            *building = None;
112        }
113    }
114
115    /// The revision the index is exact for, or `None` when there is none yet.
116    pub fn revision(&self) -> Option<u64> {
117        self.index.try_borrow().ok()?.as_ref().map(PackageIndex::revision)
118    }
119
120    /// How many nodes the index holds; `None` when there is no index.
121    pub fn node_count(&self) -> Option<usize> {
122        self.index.try_borrow().ok()?.as_ref().map(PackageIndex::len)
123    }
124
125    /// Whether the index covers the store's whole inventory; `None` when there
126    /// is no index.
127    pub fn is_complete(&self) -> Option<bool> {
128        self.index
129            .try_borrow()
130            .ok()?
131            .as_ref()
132            .map(PackageIndex::is_complete)
133    }
134
135    /// What the updates of the index disturbed since this consumer last asked -
136    /// and forget it. A cache of answers derived from the links asks this
137    /// **inside** [`IndexSlot::with`], once the index is exact for the store, and
138    /// drops what it names (`color_walks::ColourCache::disturb`,
139    /// `attachments::Cache::disturb`).
140    pub fn take_disturbed(&self, consumer: Consumer) -> Disturbed {
141        std::mem::take(&mut *self.disturbed[consumer as usize].borrow_mut())
142    }
143
144    /// Say something to **every** consumer's account.
145    fn note(&self, say: impl Fn(&mut Disturbed)) {
146        for account in &self.disturbed {
147            let mut account = account.borrow_mut();
148            say(&mut account);
149            if account.uuids.len() > DISTURBED_CAP {
150                account.uuids.sort_unstable();
151                account.uuids.dedup();
152                if account.uuids.len() > DISTURBED_CAP {
153                    account.uuids.clear();
154                    account.everything = true;
155                }
156            }
157        }
158    }
159
160    /// Run `f` on an index that is exact for `store` - **the invalidation rule
161    /// of ADR-0010**.
162    ///
163    /// No index: build one, in one call. A pass started by
164    /// [`IndexSlot::step`] and unfinished: finish it, then check (the store may
165    /// have moved since the pass began; the check below re-reads exactly those
166    /// objects). Otherwise ask the store what changed since the index's
167    /// revision: nothing, these uuids, or everything.
168    ///
169    /// An ETP store is an `EpcStore` like another here; see
170    /// [`IndexSlot::set_partial`] for what it must say about itself.
171    pub fn with<R>(&self, store: &EpcStore, f: impl FnOnce(&PackageIndex) -> R) -> R {
172        self.settle(store);
173        match self.index.try_borrow() {
174            Ok(slot) => match slot.as_ref() {
175                Some(index) => f(index),
176                None => f(&PackageIndex::build(store, store.revision())),
177            },
178            // Mutably borrowed: only `tamper` does that, and it does not call
179            // back. Answer from a fresh index rather than panic.
180            Err(_) => f(&PackageIndex::build(store, store.revision())),
181        }
182    }
183
184    /// Advance the pass by `n` objects, and say how far it is - the
185    /// worker loops, yielding between two slices, so a `mesh` or a click is
186    /// answered in the middle of a long pass.
187    ///
188    /// Begins a pass when there is no index, or when the store answers
189    /// *everything* changed. When the index exists and only some objects
190    /// changed, they are re-read at once (that is object-by-object and cheap)
191    /// and the answer is *done*. The index is installed the moment the last
192    /// slice is read; a question asked before then finishes the pass itself
193    /// ([`IndexSlot::with`]), it never answers from half an index.
194    pub fn step(&self, store: &EpcStore, n: usize) -> Progress {
195        let Ok(mut building) = self.builder.try_borrow_mut() else {
196            return Progress { done: 0, total: 0 };
197        };
198        if building.is_none() {
199            let begin = match self.index.try_borrow() {
200                Ok(slot) => match slot.as_ref() {
201                    None => true,
202                    Some(index) => matches!(store.changed_since(index.revision()), Changed::Everything),
203                },
204                Err(_) => false,
205            };
206            if begin {
207                *building = Some(IndexBuilder::new(store.resources(), store.revision()));
208            } else {
209                drop(building);
210                self.settle(store);
211                let held = self
212                    .index
213                    .try_borrow()
214                    .ok()
215                    .and_then(|slot| slot.as_ref().map(PackageIndex::len))
216                    .unwrap_or(0);
217                return Progress {
218                    done: held,
219                    total: held,
220                };
221            }
222        }
223        let Some(builder) = building.as_mut() else {
224            return Progress { done: 0, total: 0 };
225        };
226        let progress = builder.step(store, n);
227        if progress.is_done()
228            && let Some(done) = building.take()
229        {
230            self.install(done.finish());
231        }
232        progress
233    }
234
235    /// **Test sabotage**: change the index in place, so that a test can show its
236    /// confrontation with the direct readers *can* fail. Brings the index up to
237    /// date first, so the store's revision does not undo the damage on the next
238    /// question. `None` when there is no index to damage.
239    #[doc(hidden)]
240    pub fn tamper<R>(&self, store: &EpcStore, f: impl FnOnce(&mut PackageIndex) -> R) -> Option<R> {
241        self.settle(store);
242        let mut slot = self.index.try_borrow_mut().ok()?;
243        slot.as_mut().map(f)
244    }
245
246    fn install(&self, mut index: PackageIndex) {
247        // A whole new index: nothing derived from the one before it is known to hold.
248        self.note(|dirty| dirty.everything = true);
249        if self.partial.get() {
250            index.mark_incomplete();
251        }
252        if let Ok(mut slot) = self.index.try_borrow_mut() {
253            *slot = Some(index);
254        }
255    }
256
257    /// Bring the index to the store's revision, building or updating it.
258    fn settle(&self, store: &EpcStore) {
259        let unfinished = match self.builder.try_borrow_mut() {
260            Ok(mut building) => building.take(),
261            Err(_) => None,
262        };
263        if let Some(mut builder) = unfinished {
264            let total = builder.total();
265            builder.step(store, total);
266            self.install(builder.finish());
267        }
268
269        let plan = match self.index.try_borrow() {
270            Ok(slot) => match slot.as_ref() {
271                None => Plan::Build,
272                Some(index) => match store.changed_since(index.revision()) {
273                    Changed::Nothing => Plan::Keep,
274                    Changed::Some(uuids) => Plan::Update(uuids.into_iter().collect()),
275                    Changed::Everything => Plan::Build,
276                },
277            },
278            // Held by a question being answered: it is exact for this call.
279            Err(_) => Plan::Keep,
280        };
281        match plan {
282            Plan::Keep => {}
283            Plan::Build => self.install(PackageIndex::build(store, store.revision())),
284            Plan::Update(uuids) => {
285                if let Ok(mut slot) = self.index.try_borrow_mut()
286                    && let Some(index) = slot.as_mut()
287                {
288                    let disturbed = index.update(store, &uuids, store.revision());
289                    self.note(|dirty| dirty.uuids.extend(disturbed.iter().cloned()));
290                }
291            }
292        }
293    }
294}