Skip to main content

hidane_core/store/
memory.rs

1//! In-memory engine: ordered maps of versioned documents, guarded per database.
2
3use std::{
4    collections::{BTreeMap, BTreeSet, HashMap},
5    ops::{Bound, ControlFlow},
6    sync::{Arc, PoisonError, RwLock},
7    time::{Duration, SystemTime, UNIX_EPOCH},
8};
9
10use hidane_proto::ordered::Fields;
11
12use super::{
13    Change, Commit, ListItem, ListOptions, ReadTime, Store, StoreError, StoredDocument, Visit,
14    WriteBatch,
15};
16use crate::{
17    key::{encode_escaped, encode_path, encode_path_prefix, path_subtree_end},
18    path::ResourcePath,
19};
20
21type Clock = Box<dyn Fn() -> i64 + Send + Sync>;
22
23/// How far back reads may go by default. Firestore serves stale reads up to one hour old.
24pub const DEFAULT_RETENTION: Duration = Duration::from_secs(60 * 60);
25
26pub struct MemoryStore {
27    databases: RwLock<HashMap<String, Arc<RwLock<Database>>>>,
28    clock: Clock,
29    retention_micros: i64,
30}
31
32#[derive(Default)]
33struct Database {
34    /// Commit time of the last commit, in microseconds.
35    last_commit: i64,
36    /// Keyed by `encode_path(document path)`: iteration order is `__name__` order.
37    documents: BTreeMap<Vec<u8>, Record>,
38    /// `encode_escaped(collection id) ++ encode_path(document path)` for every key in
39    /// `documents`, so a collection group is one key range in full-path order.
40    groups: BTreeSet<Vec<u8>>,
41}
42
43impl Database {
44    /// Whether any document at or below `prefix` exists at `time`.
45    fn subtree_has_live(&self, prefix: &[u8], time: i64) -> bool {
46        self.documents
47            .range(prefix.to_vec()..path_subtree_end(prefix))
48            .any(|(_, record)| record.at(time).is_some())
49    }
50}
51
52struct Record {
53    path: Arc<ResourcePath>,
54    /// Ascending by commit time; `None` marks a deletion.
55    versions: Vec<(i64, Option<Arc<StoredDocument>>)>,
56}
57
58impl Record {
59    fn at(&self, time: i64) -> Option<&Arc<StoredDocument>> {
60        let newer = self.versions.partition_point(|(t, _)| *t <= time);
61        newer
62            .checked_sub(1)
63            .and_then(|i| self.versions[i].1.as_ref())
64    }
65
66    fn latest(&self) -> Option<&Arc<StoredDocument>> {
67        self.versions.last().and_then(|(_, doc)| doc.as_ref())
68    }
69
70    /// The commit time of the deletion that ended the document, if its latest version is one.
71    fn deleted_at(&self) -> Option<i64> {
72        match self.versions.last() {
73            Some((time, None)) => Some(*time),
74            _ => None,
75        }
76    }
77
78    /// Drops versions no read can reach any more: everything older than the newest version at
79    /// or before `cutoff`.
80    fn prune(&mut self, cutoff: i64) {
81        let reachable_from = self.versions.partition_point(|(t, _)| *t <= cutoff);
82        if reachable_from > 1 {
83            self.versions.drain(..reachable_from - 1);
84        }
85    }
86}
87
88impl Default for MemoryStore {
89    fn default() -> Self {
90        Self::new()
91    }
92}
93
94impl MemoryStore {
95    pub fn new() -> Self {
96        Self::with_clock(system_micros)
97    }
98
99    /// A store whose "now" comes from `clock` (microseconds since the epoch). Commit times are
100    /// still strictly increasing if the clock stands still or goes backwards.
101    pub fn with_clock(clock: impl Fn() -> i64 + Send + Sync + 'static) -> Self {
102        Self {
103            databases: RwLock::default(),
104            clock: Box::new(clock),
105            retention_micros: micros(DEFAULT_RETENTION),
106        }
107    }
108
109    /// How long old versions stay readable after they are overwritten.
110    #[must_use]
111    pub fn with_retention(mut self, retention: Duration) -> Self {
112        self.retention_micros = micros(retention);
113        self
114    }
115
116    fn existing(&self, database: &str) -> Option<Arc<RwLock<Database>>> {
117        self.databases
118            .read()
119            .unwrap_or_else(PoisonError::into_inner)
120            .get(database)
121            .cloned()
122    }
123
124    fn get_or_create(&self, database: &str) -> Arc<RwLock<Database>> {
125        if let Some(db) = self.existing(database) {
126            return db;
127        }
128        self.databases
129            .write()
130            .unwrap_or_else(PoisonError::into_inner)
131            .entry(database.to_owned())
132            .or_default()
133            .clone()
134    }
135
136    fn read<R>(&self, database: &str, empty: R, f: impl FnOnce(&Database) -> R) -> R {
137        match self.existing(database) {
138            Some(db) => f(&db.read().unwrap_or_else(PoisonError::into_inner)),
139            None => empty,
140        }
141    }
142}
143
144fn system_micros() -> i64 {
145    SystemTime::now()
146        .duration_since(UNIX_EPOCH)
147        .map_or(0, |d| i64::try_from(d.as_micros()).unwrap_or(i64::MAX))
148}
149
150fn micros(d: Duration) -> i64 {
151    i64::try_from(d.as_micros()).unwrap_or(i64::MAX)
152}
153
154impl Store for MemoryStore {
155    fn latest_read_time(&self, database: &str) -> ReadTime {
156        let last = self.read(database, 0, |db| db.last_commit);
157        ReadTime((self.clock)().max(last))
158    }
159
160    fn earliest_read_time(&self, database: &str) -> ReadTime {
161        ReadTime(
162            self.latest_read_time(database)
163                .0
164                .saturating_sub(self.retention_micros),
165        )
166    }
167
168    fn get(
169        &self,
170        database: &str,
171        path: &ResourcePath,
172        at: ReadTime,
173    ) -> Option<Arc<StoredDocument>> {
174        self.read(database, None, |db| {
175            db.documents
176                .get(&encode_path(path.segments()))
177                .and_then(|record| record.at(at.0))
178                .cloned()
179        })
180    }
181
182    fn list_collection(
183        &self,
184        database: &str,
185        collection: &ResourcePath,
186        at: ReadTime,
187        options: ListOptions<'_>,
188        visit: &mut dyn FnMut(ListItem<'_>) -> ControlFlow<()>,
189    ) {
190        self.read(database, (), |db| {
191            let prefix = encode_path_prefix(collection.segments());
192            let end = Bound::Excluded(path_subtree_end(&prefix));
193            let depth = collection.len() + 1;
194            let mut start = match options.after {
195                Some(after) => {
196                    Bound::Included(path_subtree_end(&encode_path_prefix(after.segments())))
197                }
198                None => Bound::Included(prefix),
199            };
200            'scan: loop {
201                // Each pass handles one child of the collection, then seeks past its subtree.
202                let Some((_, record)) = db.documents.range((start.clone(), end.clone())).next()
203                else {
204                    return;
205                };
206                let child = encode_path_prefix(record.path.segments().take(depth));
207                let live = (record.path.len() == depth)
208                    .then(|| record.at(at.0))
209                    .flatten();
210                let flow = match live {
211                    Some(doc) => visit(ListItem::Document(doc)),
212                    None if options.include_missing && db.subtree_has_live(&child, at.0) => {
213                        let path = ResourcePath::from_segments(record.path.segments().take(depth));
214                        visit(ListItem::Missing(&path))
215                    }
216                    None => ControlFlow::Continue(()),
217                };
218                if flow.is_break() {
219                    return;
220                }
221                start = Bound::Included(path_subtree_end(&child));
222                continue 'scan;
223            }
224        });
225    }
226
227    fn scan_collection_group(
228        &self,
229        database: &str,
230        parent: &ResourcePath,
231        collection_id: &str,
232        at: ReadTime,
233        visit: &mut Visit<'_>,
234    ) {
235        self.read(database, (), |db| {
236            let mut prefix = encode_escaped(collection_id.as_bytes());
237            let group_len = prefix.len();
238            prefix.extend(encode_path_prefix(parent.segments()));
239            let end = path_subtree_end(&prefix);
240            for entry in db.groups.range(prefix..end) {
241                let doc = db
242                    .documents
243                    .get(&entry[group_len..])
244                    // The parent itself is not under the parent.
245                    .filter(|record| record.path.len() > parent.len())
246                    .and_then(|record| record.at(at.0));
247                if let Some(doc) = doc
248                    && visit(doc).is_break()
249                {
250                    return;
251                }
252            }
253        });
254    }
255
256    fn scan_descendants(
257        &self,
258        database: &str,
259        parent: &ResourcePath,
260        at: ReadTime,
261        visit: &mut Visit<'_>,
262    ) {
263        self.read(database, (), |db| {
264            let start = Bound::Excluded(encode_path(parent.segments()));
265            let end = Bound::Excluded(path_subtree_end(&encode_path_prefix(parent.segments())));
266            for (_, record) in db.documents.range((start, end)) {
267                if let Some(doc) = record.at(at.0)
268                    && visit(doc).is_break()
269                {
270                    return;
271                }
272            }
273        });
274    }
275
276    fn list_collection_ids(
277        &self,
278        database: &str,
279        parent: &ResourcePath,
280        at: ReadTime,
281    ) -> Vec<String> {
282        self.read(database, Vec::new(), |db| {
283            let prefix = encode_path_prefix(parent.segments());
284            let end = Bound::Excluded(path_subtree_end(&prefix));
285            let mut start = Bound::Excluded(encode_path(parent.segments()));
286            let mut ids = Vec::new();
287            'scan: loop {
288                for (_, record) in db.documents.range((start.clone(), end.clone())) {
289                    if record.at(at.0).is_none() {
290                        continue;
291                    }
292                    // A live document somewhere under this collection: record the collection
293                    // and skip the rest of it.
294                    let collection = record.path.segments().take(parent.len() + 1);
295                    if let Some(id) = collection.clone().last() {
296                        ids.push(id.to_owned());
297                    }
298                    let collection_prefix = encode_path_prefix(collection);
299                    start = Bound::Included(path_subtree_end(&collection_prefix));
300                    continue 'scan;
301                }
302                return ids;
303            }
304        })
305    }
306
307    fn commit(
308        &self,
309        database: &str,
310        write: &mut dyn FnMut(&mut dyn WriteBatch) -> Result<(), StoreError>,
311    ) -> Result<Commit, StoreError> {
312        let db = self.get_or_create(database);
313        let mut db = db.write().unwrap_or_else(PoisonError::into_inner);
314        let commit_time = (self.clock)().max(db.last_commit + 1);
315
316        let mut batch = MemoryBatch {
317            db: &db,
318            commit_time,
319            pending: BTreeMap::new(),
320        };
321        write(&mut batch)?;
322        let pending = batch.pending;
323
324        let cutoff = commit_time.saturating_sub(self.retention_micros);
325        let mut changes = Vec::with_capacity(pending.len());
326        for (key, (path, after)) in pending {
327            let before = db.documents.get(&key).and_then(Record::latest).cloned();
328            if before.is_none() && after.is_none() {
329                continue; // deleting a document that does not exist changes nothing
330            }
331            if !db.documents.contains_key(&key)
332                && let Some(collection_id) = path.collection_id()
333            {
334                let mut group_key = encode_escaped(collection_id.as_bytes());
335                group_key.extend_from_slice(&key);
336                db.groups.insert(group_key);
337            }
338            let record = db.documents.entry(key).or_insert_with(|| Record {
339                path: Arc::clone(&path),
340                // Most documents never get a second version inside the retention window.
341                versions: Vec::with_capacity(1),
342            });
343            record.versions.push((commit_time, after.clone()));
344            record.prune(cutoff);
345            changes.push(Change {
346                path,
347                before,
348                after,
349            });
350        }
351        db.last_commit = commit_time;
352        Ok(Commit {
353            commit_time: ReadTime(commit_time),
354            changes,
355        })
356    }
357
358    fn clear_database(&self, database: &str) -> ReadTime {
359        let db = self.get_or_create(database);
360        let mut db = db.write().unwrap_or_else(PoisonError::into_inner);
361        let at = (self.clock)().max(db.last_commit + 1);
362        db.documents.clear();
363        db.groups.clear();
364        db.last_commit = at;
365        ReadTime(at)
366    }
367
368    fn database_names(&self) -> Vec<String> {
369        self.databases
370            .read()
371            .unwrap_or_else(PoisonError::into_inner)
372            .keys()
373            .cloned()
374            .collect()
375    }
376
377    fn clear(&self) {
378        self.databases
379            .write()
380            .unwrap_or_else(PoisonError::into_inner)
381            .clear();
382    }
383}
384
385/// A pending write: the document's shared path and its new version (`None` deletes it).
386type PendingWrite = (Arc<ResourcePath>, Option<Arc<StoredDocument>>);
387
388struct MemoryBatch<'a> {
389    db: &'a Database,
390    commit_time: i64,
391    /// Keyed like `Database::documents`.
392    pending: BTreeMap<Vec<u8>, PendingWrite>,
393}
394
395impl MemoryBatch<'_> {
396    /// The path as already shared by the document's earlier versions, if any.
397    fn shared_path(&self, key: &[u8], path: &ResourcePath) -> Arc<ResourcePath> {
398        self.pending
399            .get(key)
400            .map(|(p, _)| Arc::clone(p))
401            .or_else(|| self.db.documents.get(key).map(|r| Arc::clone(&r.path)))
402            .unwrap_or_else(|| Arc::new(path.clone()))
403    }
404}
405
406impl WriteBatch for MemoryBatch<'_> {
407    fn commit_time(&self) -> ReadTime {
408        ReadTime(self.commit_time)
409    }
410
411    fn get(&self, path: &ResourcePath) -> Option<Arc<StoredDocument>> {
412        let key = encode_path(path.segments());
413        match self.pending.get(&key) {
414            Some((_, pending)) => pending.clone(),
415            None => self
416                .db
417                .documents
418                .get(&key)
419                .and_then(Record::latest)
420                .cloned(),
421        }
422    }
423
424    fn set(&mut self, path: &ResourcePath, fields: Fields) {
425        let now = ReadTime(self.commit_time).to_timestamp();
426        let create_time = self
427            .get(path)
428            .map_or_else(|| now, |existing| existing.create_time);
429        let key = encode_path(path.segments());
430        let shared = self.shared_path(&key, path);
431        let document = StoredDocument::new(Arc::clone(&shared), fields, create_time, now);
432        self.pending.insert(key, (shared, Some(Arc::new(document))));
433    }
434
435    fn delete(&mut self, path: &ResourcePath) {
436        let key = encode_path(path.segments());
437        let shared = self.shared_path(&key, path);
438        self.pending.insert(key, (shared, None));
439    }
440
441    fn revert(&mut self, path: &ResourcePath) {
442        self.pending.remove(&encode_path(path.segments()));
443    }
444
445    fn deleted_at(&self, path: &ResourcePath) -> Option<ReadTime> {
446        let key = encode_path(path.segments());
447        let record = self.db.documents.get(&key);
448        let committed = || record.and_then(Record::deleted_at).map(ReadTime);
449        match self.pending.get(&key) {
450            Some((_, Some(_))) => None,
451            // This batch deletes it: now, if it existed; else whenever it was deleted before.
452            Some((_, None)) if record.and_then(Record::latest).is_some() => {
453                Some(ReadTime(self.commit_time))
454            }
455            Some((_, None)) | None => committed(),
456        }
457    }
458}
459
460#[cfg(test)]
461mod tests;