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}