Skip to main content

sui_castore/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::StoreError;
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    ///
30    /// # Errors
31    ///
32    /// Returns [`StoreError::Io`] if the database cannot be opened or the
33    /// schema tables cannot be created.
34    pub fn open(path: &Path) -> Result<Self, StoreError> {
35        let db = Database::create(path)
36            .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb open: {e}"))))?;
37
38        // Ensure tables exist
39        let write_txn = db
40            .begin_write()
41            .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb txn: {e}"))))?;
42        {
43            let _ = write_txn.open_table(NARINFO_TABLE);
44            let _ = write_txn.open_table(STORE_PATH_TABLE);
45        }
46        write_txn
47            .commit()
48            .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb commit: {e}"))))?;
49
50        info!(path = %path.display(), "Opened redb index");
51        Ok(Self { db })
52    }
53
54    /// Record a narinfo in the index.
55    ///
56    /// # Errors
57    ///
58    /// Returns [`StoreError::Io`] on a redb write failure.
59    pub fn index_narinfo(&self, hash: &str, nar_size: u64) -> Result<(), StoreError> {
60        let now = std::time::SystemTime::now()
61            .duration_since(std::time::UNIX_EPOCH)
62            .unwrap_or_default()
63            .as_secs();
64
65        let write_txn = self.db.begin_write()
66            .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb txn: {e}"))))?;
67        {
68            let mut table = write_txn.open_table(NARINFO_TABLE)
69                .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb table: {e}"))))?;
70            table.insert(hash, (now, nar_size))
71                .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb insert: {e}"))))?;
72        }
73        write_txn.commit()
74            .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb commit: {e}"))))?;
75
76        debug!(hash = %hash, nar_size, "Indexed narinfo");
77        Ok(())
78    }
79
80    /// Record a store path → hash mapping.
81    ///
82    /// # Errors
83    ///
84    /// Returns [`StoreError::Io`] on a redb write failure.
85    pub fn index_store_path(&self, store_path: &str, hash: &str) -> Result<(), StoreError> {
86        let write_txn = self.db.begin_write()
87            .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb txn: {e}"))))?;
88        {
89            let mut table = write_txn.open_table(STORE_PATH_TABLE)
90                .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb table: {e}"))))?;
91            table.insert(store_path, hash)
92                .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb insert: {e}"))))?;
93        }
94        write_txn.commit()
95            .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb commit: {e}"))))?;
96        Ok(())
97    }
98
99    /// Check if a hash exists in the index.
100    ///
101    /// # Errors
102    ///
103    /// Returns [`StoreError::Io`] on a redb read failure.
104    pub fn has_narinfo(&self, hash: &str) -> Result<bool, StoreError> {
105        let read_txn = self.db.begin_read()
106            .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb txn: {e}"))))?;
107        let table = read_txn.open_table(NARINFO_TABLE)
108            .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb table: {e}"))))?;
109        let exists = table.get(hash)
110            .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb get: {e}"))))?
111            .is_some();
112        Ok(exists)
113    }
114
115    /// List all indexed hashes.
116    ///
117    /// # Errors
118    ///
119    /// Returns [`StoreError::Io`] on a redb read failure.
120    pub fn list_hashes(&self) -> Result<Vec<String>, StoreError> {
121        let read_txn = self.db.begin_read()
122            .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb txn: {e}"))))?;
123        let table = read_txn.open_table(NARINFO_TABLE)
124            .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb table: {e}"))))?;
125
126        let mut hashes = Vec::new();
127        let iter = table.iter()
128            .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb iter: {e}"))))?;
129        for entry in iter {
130            let (key, _) = entry
131                .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb entry: {e}"))))?;
132            hashes.push(key.value().to_string());
133        }
134        Ok(hashes)
135    }
136
137    /// Total number of indexed narinfos.
138    ///
139    /// # Errors
140    ///
141    /// Returns [`StoreError::Io`] on a redb read failure.
142    pub fn count(&self) -> Result<u64, StoreError> {
143        let read_txn = self.db.begin_read()
144            .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb txn: {e}"))))?;
145        let table = read_txn.open_table(NARINFO_TABLE)
146            .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb table: {e}"))))?;
147        table.len()
148            .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb len: {e}"))))
149    }
150
151    /// Remove a hash from the index.
152    ///
153    /// # Errors
154    ///
155    /// Returns [`StoreError::Io`] on a redb write failure.
156    pub fn remove(&self, hash: &str) -> Result<(), StoreError> {
157        let write_txn = self.db.begin_write()
158            .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb txn: {e}"))))?;
159        {
160            let mut table = write_txn.open_table(NARINFO_TABLE)
161                .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb table: {e}"))))?;
162            let _ = table.remove(hash);
163        }
164        write_txn.commit()
165            .map_err(|e| StoreError::Io(std::io::Error::other(format!("redb commit: {e}"))))?;
166        Ok(())
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn index_roundtrip() {
176        let dir = tempfile::tempdir().unwrap();
177        let db_path = dir.path().join("test.redb");
178        let idx = StorageIndex::open(&db_path).unwrap();
179
180        // Index a narinfo
181        idx.index_narinfo("abc123", 1024).unwrap();
182        assert!(idx.has_narinfo("abc123").unwrap());
183        assert!(!idx.has_narinfo("xyz789").unwrap());
184        assert_eq!(idx.count().unwrap(), 1);
185
186        // List
187        let hashes = idx.list_hashes().unwrap();
188        assert_eq!(hashes, vec!["abc123"]);
189
190        // Remove
191        idx.remove("abc123").unwrap();
192        assert!(!idx.has_narinfo("abc123").unwrap());
193        assert_eq!(idx.count().unwrap(), 0);
194    }
195
196    #[test]
197    fn store_path_mapping() {
198        let dir = tempfile::tempdir().unwrap();
199        let db_path = dir.path().join("test.redb");
200        let idx = StorageIndex::open(&db_path).unwrap();
201
202        idx.index_store_path("abc123-hello", "abc123").unwrap();
203    }
204}