Skip to main content

sui_cache/storage/
index.rs

1//! redb metadata index — ephemeral local cache for S3 narinfo lookups.
2//!
3//! The index is disposable: when a sui pod scales to zero and back,
4//! the redb file is gone. On cold start, the index rebuilds from S3
5//! narinfo listings. This makes the index fully breathable — zero state
6//! between scale events, S3 is the durable source of truth.
7
8use std::path::Path;
9
10use redb::{Database, ReadableTable, ReadableTableMetadata, TableDefinition};
11use tracing::{debug, info};
12
13use crate::CacheError;
14
15// Table definitions
16const NARINFO_TABLE: TableDefinition<&str, (u64, u64)> = TableDefinition::new("narinfos");
17// Key: 32-char hash, Value: (timestamp_secs, nar_size)
18
19const STORE_PATH_TABLE: TableDefinition<&str, &str> = TableDefinition::new("store_paths");
20// Key: store path basename, Value: 32-char hash
21
22/// Ephemeral metadata index backed by redb.
23pub struct StorageIndex {
24    db: Database,
25}
26
27impl StorageIndex {
28    /// Open or create the index at the given path.
29    pub fn open(path: &Path) -> Result<Self, CacheError> {
30        let db = Database::create(path)
31            .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb open: {e}"))))?;
32
33        // Ensure tables exist
34        let write_txn = db
35            .begin_write()
36            .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb txn: {e}"))))?;
37        {
38            let _ = write_txn.open_table(NARINFO_TABLE);
39            let _ = write_txn.open_table(STORE_PATH_TABLE);
40        }
41        write_txn
42            .commit()
43            .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb commit: {e}"))))?;
44
45        info!(path = %path.display(), "Opened redb index");
46        Ok(Self { db })
47    }
48
49    /// Record a narinfo in the index.
50    pub fn index_narinfo(&self, hash: &str, nar_size: u64) -> Result<(), CacheError> {
51        let now = std::time::SystemTime::now()
52            .duration_since(std::time::UNIX_EPOCH)
53            .unwrap_or_default()
54            .as_secs();
55
56        let write_txn = self.db.begin_write()
57            .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb txn: {e}"))))?;
58        {
59            let mut table = write_txn.open_table(NARINFO_TABLE)
60                .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb table: {e}"))))?;
61            table.insert(hash, (now, nar_size))
62                .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb insert: {e}"))))?;
63        }
64        write_txn.commit()
65            .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb commit: {e}"))))?;
66
67        debug!(hash = %hash, nar_size, "Indexed narinfo");
68        Ok(())
69    }
70
71    /// Record a store path → hash mapping.
72    pub fn index_store_path(&self, store_path: &str, hash: &str) -> Result<(), CacheError> {
73        let write_txn = self.db.begin_write()
74            .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb txn: {e}"))))?;
75        {
76            let mut table = write_txn.open_table(STORE_PATH_TABLE)
77                .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb table: {e}"))))?;
78            table.insert(store_path, hash)
79                .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb insert: {e}"))))?;
80        }
81        write_txn.commit()
82            .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb commit: {e}"))))?;
83        Ok(())
84    }
85
86    /// Check if a hash exists in the index.
87    pub fn has_narinfo(&self, hash: &str) -> Result<bool, CacheError> {
88        let read_txn = self.db.begin_read()
89            .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb txn: {e}"))))?;
90        let table = read_txn.open_table(NARINFO_TABLE)
91            .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb table: {e}"))))?;
92        let exists = table.get(hash)
93            .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb get: {e}"))))?
94            .is_some();
95        Ok(exists)
96    }
97
98    /// List all indexed hashes.
99    pub fn list_hashes(&self) -> Result<Vec<String>, CacheError> {
100        let read_txn = self.db.begin_read()
101            .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb txn: {e}"))))?;
102        let table = read_txn.open_table(NARINFO_TABLE)
103            .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb table: {e}"))))?;
104
105        let mut hashes = Vec::new();
106        let iter = table.iter()
107            .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb iter: {e}"))))?;
108        for entry in iter {
109            let (key, _) = entry
110                .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb entry: {e}"))))?;
111            hashes.push(key.value().to_string());
112        }
113        Ok(hashes)
114    }
115
116    /// Total number of indexed narinfos.
117    pub fn count(&self) -> Result<u64, CacheError> {
118        let read_txn = self.db.begin_read()
119            .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb txn: {e}"))))?;
120        let table = read_txn.open_table(NARINFO_TABLE)
121            .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb table: {e}"))))?;
122        table.len()
123            .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb len: {e}"))))
124    }
125
126    /// Remove a hash from the index.
127    pub fn remove(&self, hash: &str) -> Result<(), CacheError> {
128        let write_txn = self.db.begin_write()
129            .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb txn: {e}"))))?;
130        {
131            let mut table = write_txn.open_table(NARINFO_TABLE)
132                .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb table: {e}"))))?;
133            let _ = table.remove(hash);
134        }
135        write_txn.commit()
136            .map_err(|e| CacheError::Io(std::io::Error::other(format!("redb commit: {e}"))))?;
137        Ok(())
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn index_roundtrip() {
147        let dir = tempfile::tempdir().unwrap();
148        let db_path = dir.path().join("test.redb");
149        let idx = StorageIndex::open(&db_path).unwrap();
150
151        // Index a narinfo
152        idx.index_narinfo("abc123", 1024).unwrap();
153        assert!(idx.has_narinfo("abc123").unwrap());
154        assert!(!idx.has_narinfo("xyz789").unwrap());
155        assert_eq!(idx.count().unwrap(), 1);
156
157        // List
158        let hashes = idx.list_hashes().unwrap();
159        assert_eq!(hashes, vec!["abc123"]);
160
161        // Remove
162        idx.remove("abc123").unwrap();
163        assert!(!idx.has_narinfo("abc123").unwrap());
164        assert_eq!(idx.count().unwrap(), 0);
165    }
166
167    #[test]
168    fn store_path_mapping() {
169        let dir = tempfile::tempdir().unwrap();
170        let db_path = dir.path().join("test.redb");
171        let idx = StorageIndex::open(&db_path).unwrap();
172
173        idx.index_store_path("abc123-hello", "abc123").unwrap();
174    }
175}