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 fn count(&self) -> StorageResult<u64>;
243
244 fn shutdown(&self) {}
248
249 fn clone_box(&self) -> Box<dyn Storage<K, V>>;
251}
252
253impl<K, V> Clone for Box<dyn Storage<K, V>> {
255 fn clone(&self) -> Self {
256 self.clone_box()
257 }
258}
259
260pub 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 pub const POSTGRES_TEST_URL_ENV: &str = "SURFPOOL_TEST_POSTGRES_URL";
282
283 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 #[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 #[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 #[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 let _ = std::fs::remove_file(db_path);
385 }
386 }
389 }
390
391 pub fn create_tmp_sqlite_storage() -> String {
392 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 let database_url = database_url.to_str().unwrap().to_string();
404 println!("Created temporary Sqlite database at: {}", database_url);
405 database_url
406 }
407}