Skip to main content

surfpool_core/storage/
mod.rs

1#[cfg(any(feature = "postgres", feature = "sqlite"))]
2mod diesel_common;
3mod fifo_map;
4mod hash_map;
5mod overlay;
6#[cfg(feature = "postgres")]
7mod postgres;
8#[cfg(feature = "sqlite")]
9mod sqlite;
10pub use hash_map::HashMap as StorageHashMap;
11pub use overlay::OverlayStorage;
12#[cfg(feature = "postgres")]
13pub use postgres::PostgresStorage;
14#[cfg(feature = "sqlite")]
15pub use sqlite::SqliteStorage;
16pub use surfpool_types::FifoMap as StorageFifoMap;
17
18use crate::error::SurfpoolError;
19
20pub fn new_kv_store<K, V>(
21    database_url: &Option<&str>,
22    table_name: &str,
23    surfnet_id: &str,
24) -> StorageResult<Box<dyn Storage<K, V>>>
25where
26    K: serde::Serialize
27        + serde::de::DeserializeOwned
28        + Send
29        + Sync
30        + 'static
31        + Clone
32        + Eq
33        + std::hash::Hash,
34    V: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static + Clone,
35{
36    new_kv_store_with_default(database_url, table_name, surfnet_id, || {
37        Box::new(StorageHashMap::new())
38    })
39}
40
41pub fn new_kv_store_with_default<K, V, F>(
42    database_url: &Option<&str>,
43    table_name: &str,
44    surfnet_id: &str,
45    default_storage_constructor: F,
46) -> StorageResult<Box<dyn Storage<K, V>>>
47where
48    K: serde::Serialize
49        + serde::de::DeserializeOwned
50        + Send
51        + Sync
52        + 'static
53        + Clone
54        + Eq
55        + std::hash::Hash,
56    V: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static + Clone,
57    F: FnOnce() -> Box<dyn Storage<K, V>>,
58{
59    match database_url {
60        Some(url) => {
61            #[cfg(feature = "postgres")]
62            if url.starts_with("postgres://") || url.starts_with("postgresql://") {
63                let storage = PostgresStorage::connect(url, table_name, surfnet_id)?;
64                Ok(Box::new(storage))
65            } else {
66                #[cfg(feature = "sqlite")]
67                {
68                    let storage = SqliteStorage::connect(url, table_name, surfnet_id)?;
69                    Ok(Box::new(storage))
70                }
71                #[cfg(not(feature = "sqlite"))]
72                {
73                    Err(StorageError::InvalidPostgresUrl(url.to_string()))
74                }
75            }
76
77            #[cfg(not(feature = "postgres"))]
78            if url.starts_with("postgres://") || url.starts_with("postgresql://") {
79                Err(StorageError::PostgresNotEnabled)
80            } else {
81                #[cfg(feature = "sqlite")]
82                {
83                    let storage = SqliteStorage::connect(
84                        database_url.unwrap_or(":memory:"),
85                        table_name,
86                        surfnet_id,
87                    )?;
88                    Ok(Box::new(storage))
89                }
90                #[cfg(not(feature = "sqlite"))]
91                {
92                    Err(StorageError::SqliteNotEnabled)
93                }
94            }
95        }
96        _ => {
97            let storage = default_storage_constructor();
98            Ok(storage)
99        }
100    }
101}
102
103#[derive(Debug, thiserror::Error)]
104pub enum StorageError {
105    #[error("Sqlite storage is not enabled in this build")]
106    SqliteNotEnabled,
107    #[error("Postgres storage is not enabled in this build")]
108    PostgresNotEnabled,
109    #[error("Invalid Postgres database URL: {0}")]
110    InvalidPostgresUrl(String),
111    #[error("Failed to get pooled connection for '{0}' database: {1}")]
112    PooledConnectionError(String, #[source] surfpool_db::diesel::r2d2::PoolError),
113    #[error("Failed to serialize key for '{0}' database: {1}")]
114    SerializeKeyError(String, serde_json::Error),
115    #[error("Failed to serialize value for '{0}' database: {1}")]
116    SerializeValueError(String, serde_json::Error),
117    #[error("Failed to deserialize value in '{0}' database: {1}")]
118    DeserializeValueError(String, serde_json::Error),
119    #[error("Failed to acquire lock for database")]
120    LockError,
121    #[error("Query failed for table '{0}' in '{1}' database: {2}")]
122    QueryError(String, String, #[source] QueryExecuteError),
123}
124
125impl StorageError {
126    pub fn create_table(
127        table_name: &str,
128        db_type: &str,
129        e: surfpool_db::diesel::result::Error,
130    ) -> Self {
131        StorageError::QueryError(
132            table_name.to_string(),
133            db_type.to_string(),
134            QueryExecuteError::CreateTableError(e),
135        )
136    }
137    pub fn store(
138        table_name: &str,
139        db_type: &str,
140        store_key: &str,
141        e: surfpool_db::diesel::result::Error,
142    ) -> Self {
143        StorageError::QueryError(
144            table_name.to_string(),
145            db_type.to_string(),
146            QueryExecuteError::StoreError(store_key.to_string(), e),
147        )
148    }
149    pub fn get(
150        table_name: &str,
151        db_type: &str,
152        get_key: &str,
153        e: surfpool_db::diesel::result::Error,
154    ) -> Self {
155        StorageError::QueryError(
156            table_name.to_string(),
157            db_type.to_string(),
158            QueryExecuteError::GetError(get_key.to_string(), e),
159        )
160    }
161    pub fn delete(
162        table_name: &str,
163        db_type: &str,
164        delete_key: &str,
165        e: surfpool_db::diesel::result::Error,
166    ) -> Self {
167        StorageError::QueryError(
168            table_name.to_string(),
169            db_type.to_string(),
170            QueryExecuteError::DeleteError(delete_key.to_string(), e),
171        )
172    }
173    pub fn get_all_keys(
174        table_name: &str,
175        db_type: &str,
176        e: surfpool_db::diesel::result::Error,
177    ) -> Self {
178        StorageError::QueryError(
179            table_name.to_string(),
180            db_type.to_string(),
181            QueryExecuteError::GetAllKeysError(e),
182        )
183    }
184    pub fn get_all_key_value_pairs(
185        table_name: &str,
186        db_type: &str,
187        e: surfpool_db::diesel::result::Error,
188    ) -> Self {
189        StorageError::QueryError(
190            table_name.to_string(),
191            db_type.to_string(),
192            QueryExecuteError::GetAllKeyValuePairsError(e),
193        )
194    }
195    pub fn count(table_name: &str, db_type: &str, e: surfpool_db::diesel::result::Error) -> Self {
196        StorageError::QueryError(
197            table_name.to_string(),
198            db_type.to_string(),
199            QueryExecuteError::CountError(e),
200        )
201    }
202}
203
204#[derive(Debug, thiserror::Error)]
205pub enum QueryExecuteError {
206    #[error("Failed to create table: {0}")]
207    CreateTableError(#[source] surfpool_db::diesel::result::Error),
208    #[error("Failed to store value for key '{0}': {1}")]
209    StoreError(String, #[source] surfpool_db::diesel::result::Error),
210    #[error("Failed to get value for key '{0}': {1}")]
211    GetError(String, #[source] surfpool_db::diesel::result::Error),
212    #[error("Failed to delete value for key '{0}': {1}")]
213    DeleteError(String, #[source] surfpool_db::diesel::result::Error),
214    #[error("Failed to get all keys: {0}")]
215    GetAllKeysError(#[source] surfpool_db::diesel::result::Error),
216    #[error("Failed to get all key-value pairs: {0}")]
217    GetAllKeyValuePairsError(#[source] surfpool_db::diesel::result::Error),
218    #[error("Failed to count entries: {0}")]
219    CountError(#[source] surfpool_db::diesel::result::Error),
220}
221
222pub type StorageResult<T> = Result<T, StorageError>;
223
224impl From<StorageError> for jsonrpc_core::Error {
225    fn from(err: StorageError) -> Self {
226        SurfpoolError::from(err).into()
227    }
228}
229
230pub trait Storage<K, V>: Send + Sync {
231    fn store(&mut self, key: K, value: V) -> StorageResult<()>;
232    fn clear(&mut self) -> StorageResult<()>;
233    fn get(&self, key: &K) -> StorageResult<Option<V>>;
234    fn take(&mut self, key: &K) -> StorageResult<Option<V>>;
235    fn keys(&self) -> StorageResult<Vec<K>>;
236    fn into_iter(&self) -> StorageResult<Box<dyn Iterator<Item = (K, V)> + '_>>;
237    fn contains_key(&self, key: &K) -> StorageResult<bool> {
238        Ok(self.get(key)?.is_some())
239    }
240
241    /// Returns the number of entries in the storage.
242    fn count(&self) -> StorageResult<u64>;
243
244    /// Explicitly shutdown the storage, performing any cleanup like WAL checkpoint.
245    /// This should be called before the application exits to ensure data is persisted.
246    /// Default implementation does nothing.
247    fn shutdown(&self) {}
248
249    // Enable cloning of boxed trait objects
250    fn clone_box(&self) -> Box<dyn Storage<K, V>>;
251}
252
253// Implement Clone for Box<dyn Storage<K, V>>
254impl<K, V> Clone for Box<dyn Storage<K, V>> {
255    fn clone(&self) -> Self {
256        self.clone_box()
257    }
258}
259
260// Separate trait for construction - this doesn't need to be dyn-compatible
261pub trait StorageConstructor<K, V>: Storage<K, V> + Clone {
262    fn connect(database_url: &str, table_name: &str, surfnet_id: &str) -> StorageResult<Self>
263    where
264        Self: Sized;
265}
266
267#[cfg(test)]
268pub mod tests {
269    use std::os::unix::fs::PermissionsExt;
270
271    use crossbeam_channel::Receiver;
272    use surfpool_types::SimnetEvent;
273    use uuid::Uuid;
274
275    use crate::surfnet::{
276        GeyserEvent,
277        svm::{SurfnetSvm, SurfnetSvmConfig},
278    };
279
280    /// Environment variable for PostgreSQL database URL used in tests
281    pub const POSTGRES_TEST_URL_ENV: &str = "SURFPOOL_TEST_POSTGRES_URL";
282
283    /// Generates a random surfnet_id
284    pub fn random_surfnet_id() -> String {
285        let uuid = Uuid::new_v4();
286        uuid.to_string()
287    }
288
289    pub enum TestType {
290        NoDb,
291        InMemorySqlite,
292        OnDiskSqlite(String),
293        /// PostgreSQL with a random surfnet_id for test isolation
294        #[cfg(feature = "postgres")]
295        Postgres {
296            url: String,
297            surfnet_id: String,
298        },
299    }
300
301    impl TestType {
302        pub fn initialize_svm(&self) -> (SurfnetSvm, Receiver<SimnetEvent>, Receiver<GeyserEvent>) {
303            match &self {
304                TestType::NoDb => SurfnetSvm::default(),
305                TestType::InMemorySqlite => SurfnetSvm::new_with_db(
306                    Some(":memory:"),
307                    SurfnetSvmConfig {
308                        surfnet_id: "0".to_string(),
309                        ..SurfnetSvmConfig::default()
310                    },
311                )
312                .unwrap(),
313                TestType::OnDiskSqlite(db_path) => SurfnetSvm::new_with_db(
314                    Some(db_path.as_ref()),
315                    SurfnetSvmConfig {
316                        surfnet_id: "0".to_string(),
317                        ..SurfnetSvmConfig::default()
318                    },
319                )
320                .unwrap(),
321                #[cfg(feature = "postgres")]
322                TestType::Postgres { url, surfnet_id } => SurfnetSvm::new_with_db(
323                    Some(url.as_ref()),
324                    SurfnetSvmConfig {
325                        surfnet_id: surfnet_id.clone(),
326                        ..SurfnetSvmConfig::default()
327                    },
328                )
329                .unwrap(),
330            }
331        }
332
333        pub fn sqlite() -> Self {
334            let database_url = crate::storage::tests::create_tmp_sqlite_storage();
335            TestType::OnDiskSqlite(database_url)
336        }
337
338        pub fn no_db() -> Self {
339            TestType::NoDb
340        }
341
342        pub fn in_memory() -> Self {
343            TestType::InMemorySqlite
344        }
345
346        /// Creates a PostgreSQL test type with a random surfnet_id for test isolation.
347        /// The database URL is read from the SURFPOOL_TEST_POSTGRES_URL environment variable.
348        /// Panics if the environment variable is not set.
349        #[cfg(feature = "postgres")]
350        pub fn postgres() -> Self {
351            let url = std::env::var(POSTGRES_TEST_URL_ENV).unwrap_or_else(|_| {
352                panic!(
353                    "PostgreSQL test URL not set. Set the {} environment variable.",
354                    POSTGRES_TEST_URL_ENV
355                )
356            });
357            let surfnet_id = random_surfnet_id();
358            println!(
359                "Created PostgreSQL test connection with surfnet_id: {}",
360                surfnet_id
361            );
362            TestType::Postgres { url, surfnet_id }
363        }
364
365        /// Creates a PostgreSQL test type with a random surfnet_id for test isolation.
366        /// Returns None if the SURFPOOL_TEST_POSTGRES_URL environment variable is not set.
367        #[cfg(feature = "postgres")]
368        pub fn postgres_if_available() -> Option<Self> {
369            std::env::var(POSTGRES_TEST_URL_ENV).ok().map(|url| {
370                let surfnet_id = random_surfnet_id();
371                println!(
372                    "Created PostgreSQL test connection with surfnet_id: {}",
373                    surfnet_id
374                );
375                TestType::Postgres { url, surfnet_id }
376            })
377        }
378    }
379
380    impl Drop for TestType {
381        fn drop(&mut self) {
382            if let TestType::OnDiskSqlite(db_path) = self {
383                // Delete file at db_path when TestType goes out of scope
384                let _ = std::fs::remove_file(db_path);
385            }
386            // Note: PostgreSQL data is isolated by surfnet_id and doesn't need cleanup
387            // The random surfnet_id ensures test isolation without table cleanup
388        }
389    }
390
391    pub fn create_tmp_sqlite_storage() -> String {
392        // let temp_dir = tempfile::tempdir().expect("Failed to create temp dir for SqliteStorage");
393        let write_permissions = std::fs::Permissions::from_mode(0o600);
394        let file = tempfile::Builder::new()
395            .permissions(write_permissions)
396            .suffix(".sqlite")
397            .tempfile()
398            .expect("Failed to create temp file for SqliteStorage");
399        let database_url = file.path().to_path_buf();
400
401        // Use a simple path without creating the file beforehand
402        // Let SQLite create the database file itself
403        let database_url = database_url.to_str().unwrap().to_string();
404        println!("Created temporary Sqlite database at: {}", database_url);
405        database_url
406    }
407}