Skip to main content

rushdb_storage/
graph_cache.rs

1//! Resident adjacency of one relationship type, cached per snapshot.
2//!
3//! A traversal reads each record's adjacency rows with one range scan per
4//! record and direction. When traversals at one snapshot keep reading the
5//! same relationships, holding them in memory is cheaper: the graph a type
6//! resolves to is a function of the snapshot's commit index, so it is kept
7//! under `(key, commit index)` and shared by every reader of that snapshot,
8//! like the owner-set cache. A commit moves readers to a new index; older
9//! graphs age out by LRU.
10//!
11//! **When to build** is a rent-or-buy decision. Building reads every
12//! adjacency row of the tenant once (the rows are keyed by record, not by
13//! type). Traversals report the rows they read through storage for a key at
14//! a commit index ([`GraphCache::rent`]); once that reaches the cost of one
15//! build, the next traversal builds and keeps the graph. A read-mostly
16//! tenant pays at most twice the build; a tenant whose commits keep moving
17//! the snapshot never builds, because its rent restarts at every commit.
18//!
19//! Charged to its own budget (`graph-cache-bytes`) under the managed limit;
20//! a graph that does not fit is simply not kept, and traversals read
21//! storage as before.
22
23use std::any::Any;
24use std::collections::HashMap;
25use std::sync::{Arc, Mutex};
26
27use crate::cache::LogicalCache;
28use crate::resources::ByteBudget;
29use crate::StorageError;
30
31/// A cached graph: opaque to storage (the query layer's adjacency type).
32pub type Graph = Arc<dyn Any + Send + Sync>;
33
34/// The store's per-snapshot adjacency cache.
35pub struct GraphCache {
36    cache: Mutex<Option<LogicalCache<(Vec<u8>, u64), Graph>>>,
37    /// Rows read through storage per key, at the commit index they were
38    /// read at.
39    rent: Mutex<HashMap<Vec<u8>, (u64, u64)>>,
40}
41
42impl GraphCache {
43    /// Build under `budget` (`graph-cache-bytes`); without one there is no
44    /// cache: every lookup misses and nothing is ever built.
45    ///
46    /// # Errors
47    /// Returns [`StorageError::InvalidConfiguration`] if the cache cannot
48    /// reserve its budget.
49    pub(crate) fn new(budget: Option<ByteBudget>) -> Result<Self, StorageError> {
50        let cache = budget
51            .map(LogicalCache::new)
52            .transpose()
53            .map_err(|e| StorageError::InvalidConfiguration(e.to_string()))?;
54        Ok(Self {
55            cache: Mutex::new(cache),
56            rent: Mutex::new(HashMap::new()),
57        })
58    }
59
60    /// Whether graphs can be kept at all.
61    #[must_use]
62    pub fn enabled(&self) -> bool {
63        self.cache.lock().is_ok_and(|cache| cache.is_some())
64    }
65
66    /// The graph cached for `key` at commit index `ci`.
67    #[must_use]
68    pub fn get(&self, key: &[u8], ci: u64) -> Option<Graph> {
69        let mut guard = self.cache.lock().ok()?;
70        guard.as_mut()?.get(&(key.to_vec(), ci)).cloned()
71    }
72
73    /// Keep `graph` (about `bytes` resident) for `key` at `ci`. Returns
74    /// whether it is now resident.
75    pub fn insert(&self, key: Vec<u8>, ci: u64, graph: Graph, bytes: usize) -> bool {
76        let Ok(mut guard) = self.cache.lock() else {
77            return false;
78        };
79        let Some(cache) = guard.as_mut() else {
80            return false;
81        };
82        // Graphs of older snapshots serve only readers still pinned there;
83        // the newest one is what later traversals use.
84        cache.retain(|(_, at), _| *at >= ci);
85        if let Ok(mut rent) = self.rent.lock() {
86            rent.remove(&key);
87        }
88        cache.insert((key, ci), graph, bytes)
89    }
90
91    /// Record `rows` adjacency rows read through storage for `key` at `ci`,
92    /// and whether the rows read at `ci` now pay for one build of
93    /// `build_rows` rows.
94    pub fn rent(&self, key: &[u8], ci: u64, rows: u64, build_rows: u64) -> bool {
95        if !self.enabled() {
96            return false;
97        }
98        let Ok(mut rent) = self.rent.lock() else {
99            return false;
100        };
101        // Rent paid at an older snapshot buys nothing at this one.
102        rent.retain(|_, (at, _)| *at >= ci);
103        let spent = rent.entry(key.to_vec()).or_insert((ci, 0));
104        if spent.0 != ci {
105            *spent = (ci, 0);
106        }
107        spent.1 = spent.1.saturating_add(rows);
108        spent.1 >= build_rows
109    }
110}