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