diff --git a/Cargo.toml b/Cargo.toml index 7b30a7a..5a69b45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/*"] resolver = "2" [workspace.package] -version = "0.6.3" +version = "0.6.4" edition = "2024" rust-version = "1.92" authors = ["init4"] @@ -35,13 +35,13 @@ incremental = false [workspace.dependencies] # internal -signet-hot = { version = "0.6.3", path = "./crates/hot" } -signet-hot-mdbx = { version = "0.6.3", path = "./crates/hot-mdbx" } -signet-cold = { version = "0.6.3", path = "./crates/cold" } -signet-cold-mdbx = { version = "0.6.3", path = "./crates/cold-mdbx" } -signet-cold-sql = { version = "0.6.3", path = "./crates/cold-sql" } -signet-storage = { version = "0.6.3", path = "./crates/storage" } -signet-storage-types = { version = "0.6.3", path = "./crates/types" } +signet-hot = { version = "0.6.4", path = "./crates/hot" } +signet-hot-mdbx = { version = "0.6.4", path = "./crates/hot-mdbx" } +signet-cold = { version = "0.6.4", path = "./crates/cold" } +signet-cold-mdbx = { version = "0.6.4", path = "./crates/cold-mdbx" } +signet-cold-sql = { version = "0.6.4", path = "./crates/cold-sql" } +signet-storage = { version = "0.6.4", path = "./crates/storage" } +signet-storage-types = { version = "0.6.4", path = "./crates/types" } # External, in-house signet-libmdbx = { version = "0.8.0" } diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml index 71293db..81c0e7c 100644 --- a/crates/storage/Cargo.toml +++ b/crates/storage/Cargo.toml @@ -17,7 +17,10 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] signet-cold.workspace = true +signet-cold-mdbx.workspace = true +signet-cold-sql = { workspace = true, optional = true } signet-hot.workspace = true +signet-hot-mdbx.workspace = true signet-storage-types.workspace = true alloy.workspace = true @@ -33,3 +36,5 @@ trevm.workspace = true [features] default = [] test-utils = ["signet-hot/test-utils", "signet-cold/test-utils"] +sql = ["signet-cold-sql/postgres"] +sqlite = ["signet-cold-sql/sqlite"] diff --git a/crates/storage/src/builder.rs b/crates/storage/src/builder.rs new file mode 100644 index 0000000..c446fc3 --- /dev/null +++ b/crates/storage/src/builder.rs @@ -0,0 +1,476 @@ +//! Storage builder for programmatic and environment-based configuration. +//! +//! This module provides a builder pattern for instantiating storage with +//! different backend combinations. The builder supports both programmatic +//! configuration and automatic loading from environment variables. +//! +//! # Examples +//! +//! ## From Environment +//! +//! ```ignore +//! use signet_storage::builder::StorageBuilder; +//! use std::env; +//! +//! // Set environment variables +//! env::set_var("STORAGE_MODE", "hot-only"); +//! env::set_var("SIGNET_HOT_PATH", "/tmp/hot"); +//! +//! // Build from environment +//! let storage = StorageBuilder::from_env()?.build()?; +//! ``` +//! +//! ## Programmatic Hot-Only +//! +//! ```ignore +//! use signet_storage::builder::{StorageBuilder, StorageInstance}; +//! use signet_storage::config::StorageMode; +//! +//! let storage = StorageBuilder::new() +//! .mode(StorageMode::HotOnly) +//! .hot_path("/tmp/hot") +//! .build()?; +//! +//! match storage { +//! StorageInstance::HotOnly(db) => { +//! // Use hot-only database +//! } +//! StorageInstance::Unified(_) => unreachable!(), +//! } +//! ``` +//! +//! ## Programmatic Hot + Cold MDBX +//! +//! ```ignore +//! use signet_storage::builder::StorageBuilder; +//! use signet_storage::config::StorageMode; +//! use tokio_util::sync::CancellationToken; +//! +//! let cancel = CancellationToken::new(); +//! let storage = StorageBuilder::new() +//! .mode(StorageMode::HotColdMdbx) +//! .hot_path("/tmp/hot") +//! .cold_path("/tmp/cold") +//! .cancel_token(cancel) +//! .build()?; +//! ``` + +use crate::{ + StorageError, StorageResult, UnifiedStorage, + config::{ConfigError, ENV_COLD_PATH, ENV_COLD_SQL_URL, ENV_HOT_PATH, StorageMode}, +}; +use signet_cold_mdbx::MdbxColdBackend; +use signet_hot_mdbx::{DatabaseArguments, DatabaseEnv}; +use std::{ + env, + path::{Path, PathBuf}, +}; +use tokio_util::sync::CancellationToken; + +/// Storage instance returned by the builder. +/// +/// This enum forces callers to handle both hot-only and unified storage cases +/// explicitly, preventing accidental calls to cold storage methods on hot-only +/// instances. +#[derive(Debug)] +pub enum StorageInstance { + /// Unified storage with both hot and cold backends. + Unified(UnifiedStorage), + /// Hot-only storage without cold backend. + HotOnly(DatabaseEnv), +} + +impl StorageInstance { + /// Get a reference to the hot database environment. + /// + /// This method works for both unified and hot-only instances. + pub const fn hot(&self) -> &DatabaseEnv { + match self { + Self::Unified(unified) => unified.hot(), + Self::HotOnly(db) => db, + } + } + + /// Convert into the hot database environment, consuming self. + /// + /// This method works for both unified and hot-only instances. + pub fn into_hot(self) -> DatabaseEnv { + match self { + Self::Unified(unified) => unified.into_hot(), + Self::HotOnly(db) => db, + } + } + + /// Get a reference to unified storage, if available. + /// + /// Returns `None` for hot-only instances. + pub const fn as_unified(&self) -> Option<&UnifiedStorage> { + match self { + Self::Unified(unified) => Some(unified), + Self::HotOnly(_) => None, + } + } + + /// Convert into unified storage, if available. + /// + /// Returns `None` for hot-only instances. + pub fn into_unified(self) -> Option> { + match self { + Self::Unified(unified) => Some(unified), + Self::HotOnly(_) => None, + } + } +} + +/// Builder for storage configuration. +/// +/// Supports both programmatic configuration and automatic loading from +/// environment variables. Use [`from_env`](Self::from_env) to load from +/// environment or [`new`](Self::new) for programmatic configuration. +#[derive(Debug)] +pub struct StorageBuilder { + mode: Option, + hot_path: Option, + cold_path: Option, + cold_sql_url: Option, + db_args: Option, + cancel_token: Option, +} + +impl Default for StorageBuilder { + fn default() -> Self { + Self::new() + } +} + +impl StorageBuilder { + /// Create a new storage builder. + /// + /// Use the setter methods to configure the builder, then call + /// [`build`](Self::build) to instantiate storage. + pub const fn new() -> Self { + Self { + mode: None, + hot_path: None, + cold_path: None, + cold_sql_url: None, + db_args: None, + cancel_token: None, + } + } + + /// Create a builder from environment variables. + /// + /// Reads configuration from: + /// - `STORAGE_MODE`: Storage mode selection + /// - `SIGNET_HOT_PATH`: Hot storage path + /// - `SIGNET_COLD_PATH`: Cold MDBX path (if applicable) + /// - `SIGNET_COLD_SQL_URL`: Cold SQL URL (if applicable) + /// + /// # Errors + /// + /// Returns an error if required environment variables are missing or invalid. + pub fn from_env() -> Result { + let mode = StorageMode::from_env()?; + + let hot_path: PathBuf = + env::var(ENV_HOT_PATH).map_err(|_| ConfigError::MissingEnvVar(ENV_HOT_PATH))?.into(); + + let mut builder = Self::new().mode(mode).hot_path(hot_path); + + match mode { + StorageMode::HotOnly => {} + StorageMode::HotColdMdbx => { + let cold_path: PathBuf = env::var(ENV_COLD_PATH) + .map_err(|_| ConfigError::MissingPath { mode, env_var: ENV_COLD_PATH })? + .into(); + builder = builder.cold_path(cold_path); + } + StorageMode::HotColdSql => { + let cold_sql_url = env::var(ENV_COLD_SQL_URL) + .map_err(|_| ConfigError::MissingPath { mode, env_var: ENV_COLD_SQL_URL })?; + builder = builder.cold_sql_url(cold_sql_url); + } + } + + Ok(builder) + } + + /// Set the storage mode. + #[must_use] + pub const fn mode(mut self, mode: StorageMode) -> Self { + self.mode = Some(mode); + self + } + + /// Set the hot storage path. + #[must_use] + pub fn hot_path(mut self, path: impl Into) -> Self { + self.hot_path = Some(path.into()); + self + } + + /// Set the cold MDBX storage path. + #[must_use] + pub fn cold_path(mut self, path: impl Into) -> Self { + self.cold_path = Some(path.into()); + self + } + + /// Set the cold SQL connection URL. + #[must_use] + pub fn cold_sql_url(mut self, url: impl Into) -> Self { + self.cold_sql_url = Some(url.into()); + self + } + + /// Set custom database arguments for MDBX backends. + /// + /// If not set, default arguments are used. + #[must_use] + pub const fn database_arguments(mut self, args: DatabaseArguments) -> Self { + self.db_args = Some(args); + self + } + + /// Set the cancellation token for cold storage tasks. + /// + /// Required for modes with cold storage. If not set, a new token is created. + #[must_use] + pub fn cancel_token(mut self, token: CancellationToken) -> Self { + self.cancel_token = Some(token); + self + } + + /// Build the storage instance. + /// + /// Opens the appropriate backends based on the configured mode and spawns + /// the cold storage task if needed. + /// + /// # Errors + /// + /// Returns an error if: + /// - Required configuration is missing + /// - Backend initialization fails + /// - Paths are invalid or inaccessible + pub fn build(self) -> StorageResult { + let mode = self + .mode + .ok_or_else(|| StorageError::Config("storage mode not configured".to_owned()))?; + + let hot_path = self + .hot_path + .ok_or_else(|| StorageError::Config("hot storage path not configured".to_owned()))?; + + let db_args = self.db_args.unwrap_or_default(); + let cancel_token = self.cancel_token.unwrap_or_default(); + + match mode { + StorageMode::HotOnly => Self::build_hot_only_impl(&hot_path, db_args), + StorageMode::HotColdMdbx => { + let cold_path = self.cold_path.ok_or_else(|| { + StorageError::Config( + "cold storage path not configured for hot-cold-mdbx mode".to_owned(), + ) + })?; + Self::build_hot_cold_mdbx_impl(&hot_path, &cold_path, db_args, cancel_token) + } + StorageMode::HotColdSql => Err(StorageError::Config( + "hot-cold-sql mode requires async initialization, use build_async() instead" + .to_owned(), + )), + } + } + + /// Build the storage instance asynchronously. + /// + /// Required for SQL-based cold storage which needs async initialization. + /// Works for all storage modes. + /// + /// # Errors + /// + /// Returns an error if: + /// - Required configuration is missing + /// - Backend initialization fails + /// - Paths or URLs are invalid or inaccessible + #[cfg(feature = "sql")] + pub async fn build_async(self) -> StorageResult { + let mode = self + .mode + .ok_or_else(|| StorageError::Config("storage mode not configured".to_owned()))?; + + let hot_path = self + .hot_path + .ok_or_else(|| StorageError::Config("hot storage path not configured".to_owned()))?; + + let db_args = self.db_args.unwrap_or_default(); + let cancel_token = self.cancel_token.unwrap_or_default(); + + match mode { + StorageMode::HotOnly => Self::build_hot_only_impl(&hot_path, db_args), + StorageMode::HotColdMdbx => { + let cold_path = self.cold_path.ok_or_else(|| { + StorageError::Config( + "cold storage path not configured for hot-cold-mdbx mode".to_owned(), + ) + })?; + Self::build_hot_cold_mdbx_impl(&hot_path, &cold_path, db_args, cancel_token) + } + StorageMode::HotColdSql => { + let cold_sql_url = self.cold_sql_url.ok_or_else(|| { + StorageError::Config( + "cold SQL URL not configured for hot-cold-sql mode".to_owned(), + ) + })?; + Self::build_hot_cold_sql_impl(&hot_path, &cold_sql_url, db_args, cancel_token).await + } + } + } + + fn build_hot_only_impl( + hot_path: &Path, + db_args: DatabaseArguments, + ) -> StorageResult { + let hot_db = db_args.open_rw(hot_path)?; + Ok(StorageInstance::HotOnly(hot_db)) + } + + fn build_hot_cold_mdbx_impl( + hot_path: &Path, + cold_path: &Path, + db_args: DatabaseArguments, + cancel_token: CancellationToken, + ) -> StorageResult { + // Open hot storage + let hot_db = db_args.open_rw(hot_path)?; + + // Open cold storage + let cold_backend = MdbxColdBackend::open_rw(cold_path)?; + + // Spawn cold storage task + let unified = UnifiedStorage::spawn(hot_db, cold_backend, cancel_token); + + Ok(StorageInstance::Unified(unified)) + } + + #[cfg(feature = "sql")] + async fn build_hot_cold_sql_impl( + hot_path: &Path, + cold_sql_url: &str, + db_args: DatabaseArguments, + cancel_token: CancellationToken, + ) -> StorageResult { + // Open hot storage + let hot_db = db_args.open_rw(hot_path)?; + + // Connect to SQL backend + let cold_backend = signet_cold_sql::SqlColdBackend::connect(cold_sql_url).await?; + + // Spawn cold storage task + let unified = UnifiedStorage::spawn(hot_db, cold_backend, cancel_token); + + Ok(StorageInstance::Unified(unified)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::ENV_STORAGE_MODE; + + #[test] + fn builder_requires_mode() { + let result = StorageBuilder::new().build(); + assert!(result.is_err()); + } + + #[test] + fn builder_requires_hot_path() { + let result = StorageBuilder::new().mode(StorageMode::HotOnly).build(); + assert!(result.is_err()); + } + + #[test] + fn from_env_missing_mode() { + // SAFETY: Test environment, single-threaded test execution + unsafe { + env::remove_var(ENV_STORAGE_MODE); + } + assert!(StorageBuilder::from_env().is_err()); + } + + #[test] + fn from_env_missing_hot_path() { + // SAFETY: Test environment, single-threaded test execution + unsafe { + env::set_var(ENV_STORAGE_MODE, "hot-only"); + env::remove_var(ENV_HOT_PATH); + } + let result = StorageBuilder::from_env(); + assert!(result.is_err()); + // SAFETY: Test environment, single-threaded test execution + unsafe { + env::remove_var(ENV_STORAGE_MODE); + } + } + + #[test] + fn from_env_hot_only_valid() { + // SAFETY: Test environment, single-threaded test execution + unsafe { + env::set_var(ENV_STORAGE_MODE, "hot-only"); + env::set_var(ENV_HOT_PATH, "/tmp/hot"); + } + + let builder = StorageBuilder::from_env().unwrap(); + assert!(matches!(builder.mode, Some(StorageMode::HotOnly))); + assert_eq!(builder.hot_path, Some(PathBuf::from("/tmp/hot"))); + + // SAFETY: Test environment, single-threaded test execution + unsafe { + env::remove_var(ENV_STORAGE_MODE); + env::remove_var(ENV_HOT_PATH); + } + } + + #[test] + fn from_env_hot_cold_mdbx_missing_cold_path() { + // SAFETY: Test environment, single-threaded test execution + unsafe { + env::set_var(ENV_STORAGE_MODE, "hot-cold-mdbx"); + env::set_var(ENV_HOT_PATH, "/tmp/hot"); + env::remove_var(ENV_COLD_PATH); + } + + let result = StorageBuilder::from_env(); + assert!(result.is_err()); + + // SAFETY: Test environment, single-threaded test execution + unsafe { + env::remove_var(ENV_STORAGE_MODE); + env::remove_var(ENV_HOT_PATH); + } + } + + #[test] + fn from_env_hot_cold_mdbx_valid() { + // SAFETY: Test environment, single-threaded test execution + unsafe { + env::set_var(ENV_STORAGE_MODE, "hot-cold-mdbx"); + env::set_var(ENV_HOT_PATH, "/tmp/hot"); + env::set_var(ENV_COLD_PATH, "/tmp/cold"); + } + + let builder = StorageBuilder::from_env().unwrap(); + assert!(matches!(builder.mode, Some(StorageMode::HotColdMdbx))); + assert_eq!(builder.hot_path, Some(PathBuf::from("/tmp/hot"))); + assert_eq!(builder.cold_path, Some(PathBuf::from("/tmp/cold"))); + + // SAFETY: Test environment, single-threaded test execution + unsafe { + env::remove_var(ENV_STORAGE_MODE); + env::remove_var(ENV_HOT_PATH); + env::remove_var(ENV_COLD_PATH); + } + } +} diff --git a/crates/storage/src/config.rs b/crates/storage/src/config.rs new file mode 100644 index 0000000..5182001 --- /dev/null +++ b/crates/storage/src/config.rs @@ -0,0 +1,209 @@ +//! Storage configuration types and environment parsing. +//! +//! This module provides configuration types for instantiating storage with +//! different backend combinations: +//! +//! - **Hot-only**: MDBX hot storage without cold storage +//! - **Hot + Cold MDBX**: Both hot and cold use MDBX backends +//! - **Hot + Cold SQL**: Hot uses MDBX, cold uses PostgreSQL or SQLite +//! +//! # Environment Variables +//! +//! | Variable | Description | Required When | +//! |----------|-------------|---------------| +//! | `STORAGE_MODE` | Storage mode (`hot-only`, `hot-cold-mdbx`, `hot-cold-sql`) | Always | +//! | `SIGNET_HOT_PATH` | Path to hot MDBX database | Always | +//! | `SIGNET_COLD_PATH` | Path to cold MDBX database | `mode=hot-cold-mdbx` | +//! | `SIGNET_COLD_SQL_URL` | SQL connection string | `mode=hot-cold-sql` | +//! +//! # Example +//! +//! ```rust +//! use signet_storage::config::StorageMode; +//! use std::env; +//! +//! // Parse from string +//! let mode: StorageMode = "hot-only".parse().unwrap(); +//! assert!(matches!(mode, StorageMode::HotOnly)); +//! +//! // Parse from environment +//! unsafe { +//! env::set_var("STORAGE_MODE", "hot-cold-mdbx"); +//! } +//! let mode = StorageMode::from_env().unwrap(); +//! assert!(matches!(mode, StorageMode::HotColdMdbx)); +//! # unsafe { env::remove_var("STORAGE_MODE"); } +//! ``` + +use std::{env, fmt, str::FromStr}; +use thiserror::Error; + +/// Environment variable name for storage mode selection. +pub const ENV_STORAGE_MODE: &str = "STORAGE_MODE"; + +/// Environment variable name for hot storage path. +pub const ENV_HOT_PATH: &str = "SIGNET_HOT_PATH"; + +/// Environment variable name for cold MDBX storage path. +pub const ENV_COLD_PATH: &str = "SIGNET_COLD_PATH"; + +/// Environment variable name for cold SQL connection URL. +pub const ENV_COLD_SQL_URL: &str = "SIGNET_COLD_SQL_URL"; + +/// Storage mode configuration. +/// +/// Defines the backend combination for storage instantiation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum StorageMode { + /// Hot-only mode: MDBX hot storage without cold storage. + HotOnly, + /// Hot + Cold MDBX: Both hot and cold use MDBX backends. + HotColdMdbx, + /// Hot MDBX + Cold SQL: Hot uses MDBX, cold uses SQL backend. + HotColdSql, +} + +impl StorageMode { + /// Load storage mode from environment variables. + /// + /// Reads the `STORAGE_MODE` environment variable and parses it. + /// + /// # Errors + /// + /// Returns [`ConfigError::MissingEnvVar`] if the environment variable is not set, + /// or [`ConfigError::InvalidMode`] if the value cannot be parsed. + /// + /// # Example + /// + /// ```rust + /// use signet_storage::config::StorageMode; + /// use std::env; + /// + /// unsafe { + /// env::set_var("STORAGE_MODE", "hot-only"); + /// } + /// let mode = StorageMode::from_env().unwrap(); + /// assert!(matches!(mode, StorageMode::HotOnly)); + /// # unsafe { env::remove_var("STORAGE_MODE"); } + /// ``` + pub fn from_env() -> Result { + let value = + env::var(ENV_STORAGE_MODE).map_err(|_| ConfigError::MissingEnvVar(ENV_STORAGE_MODE))?; + value.parse() + } +} + +impl FromStr for StorageMode { + type Err = ConfigError; + + fn from_str(s: &str) -> Result { + match s { + "hot-only" => Ok(Self::HotOnly), + "hot-cold-mdbx" => Ok(Self::HotColdMdbx), + "hot-cold-sql" => Ok(Self::HotColdSql), + _ => Err(ConfigError::InvalidMode(s.to_owned())), + } + } +} + +impl fmt::Display for StorageMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::HotOnly => write!(f, "hot-only"), + Self::HotColdMdbx => write!(f, "hot-cold-mdbx"), + Self::HotColdSql => write!(f, "hot-cold-sql"), + } + } +} + +/// Configuration errors. +#[derive(Debug, Error)] +pub enum ConfigError { + /// Required environment variable is missing. + #[error("missing environment variable: {0}")] + MissingEnvVar(&'static str), + + /// Invalid storage mode string. + #[error("invalid storage mode: {0} (expected: hot-only, hot-cold-mdbx, hot-cold-sql)")] + InvalidMode(String), + + /// Missing required path for the selected mode. + #[error("missing required path for mode {mode}: environment variable {env_var} not set")] + MissingPath { + /// The storage mode that requires the path. + mode: StorageMode, + /// The environment variable name. + env_var: &'static str, + }, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_storage_mode() { + assert_eq!("hot-only".parse::().unwrap(), StorageMode::HotOnly); + assert_eq!("hot-cold-mdbx".parse::().unwrap(), StorageMode::HotColdMdbx); + assert_eq!("hot-cold-sql".parse::().unwrap(), StorageMode::HotColdSql); + } + + #[test] + fn parse_invalid_mode() { + assert!("invalid".parse::().is_err()); + assert!("hot_only".parse::().is_err()); + assert!("".parse::().is_err()); + } + + #[test] + fn display_storage_mode() { + assert_eq!(StorageMode::HotOnly.to_string(), "hot-only"); + assert_eq!(StorageMode::HotColdMdbx.to_string(), "hot-cold-mdbx"); + assert_eq!(StorageMode::HotColdSql.to_string(), "hot-cold-sql"); + } + + #[test] + fn from_env_missing_var() { + // SAFETY: Test environment, single-threaded test execution + unsafe { + env::remove_var(ENV_STORAGE_MODE); + } + assert!(StorageMode::from_env().is_err()); + } + + #[test] + fn from_env_valid() { + // SAFETY: Test environment, single-threaded test execution + unsafe { + env::set_var(ENV_STORAGE_MODE, "hot-only"); + } + assert_eq!(StorageMode::from_env().unwrap(), StorageMode::HotOnly); + // SAFETY: Test environment, single-threaded test execution + unsafe { + env::remove_var(ENV_STORAGE_MODE); + } + + // SAFETY: Test environment, single-threaded test execution + unsafe { + env::set_var(ENV_STORAGE_MODE, "hot-cold-mdbx"); + } + assert_eq!(StorageMode::from_env().unwrap(), StorageMode::HotColdMdbx); + // SAFETY: Test environment, single-threaded test execution + unsafe { + env::remove_var(ENV_STORAGE_MODE); + } + } + + #[test] + fn from_env_invalid() { + // SAFETY: Test environment, single-threaded test execution + unsafe { + env::set_var(ENV_STORAGE_MODE, "invalid-mode"); + } + assert!(StorageMode::from_env().is_err()); + // SAFETY: Test environment, single-threaded test execution + unsafe { + env::remove_var(ENV_STORAGE_MODE); + } + } +} diff --git a/crates/storage/src/error.rs b/crates/storage/src/error.rs index a9a7d84..44e2c3c 100644 --- a/crates/storage/src/error.rs +++ b/crates/storage/src/error.rs @@ -1,7 +1,10 @@ //! Error types for unified storage operations. +use crate::config::ConfigError; use signet_cold::ColdStorageError; +use signet_cold_mdbx::MdbxColdError; use signet_hot::{HistoryError, model::HotKvError}; +use signet_hot_mdbx::MdbxError; /// Error type for unified storage operations. #[derive(Debug, thiserror::Error)] @@ -12,6 +15,19 @@ pub enum StorageError { /// Error from cold storage operations. #[error("cold storage error: {0}")] Cold(#[source] ColdStorageError), + /// Configuration error. + #[error("configuration error: {0}")] + Config(String), + /// MDBX hot storage error. + #[error("MDBX hot storage error: {0}")] + MdbxHot(#[from] MdbxError), + /// MDBX cold storage error. + #[error("MDBX cold storage error: {0}")] + MdbxCold(#[from] MdbxColdError), + /// SQL cold storage error. + #[cfg(feature = "sql")] + #[error("SQL cold storage error: {0}")] + SqlCold(#[from] signet_cold_sql::SqlColdError), } impl From> for StorageError { @@ -32,5 +48,11 @@ impl From for StorageError { } } +impl From for StorageError { + fn from(err: ConfigError) -> Self { + Self::Config(err.to_string()) + } +} + /// Result type alias for unified storage operations. pub type StorageResult = Result; diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index 5fd5bb4..60a0113 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -60,14 +60,23 @@ mod error; pub use error::{StorageError, StorageResult}; +pub mod config; + +pub mod builder; + mod unified; pub use unified::UnifiedStorage; // Re-export key types for convenience pub use signet_cold::{ColdStorage, ColdStorageError, ColdStorageHandle, ColdStorageTask}; +pub use signet_cold_mdbx::MdbxColdBackend; pub use signet_hot::{ HistoryError, HistoryRead, HistoryWrite, HotKv, model::{HotKvRead, RevmRead, RevmWrite}, }; +pub use signet_hot_mdbx::{DatabaseArguments, DatabaseEnv}; pub use signet_storage_types::{ExecutedBlock, ExecutedBlockBuilder}; pub use tokio_util::sync::CancellationToken; + +#[cfg(feature = "sql")] +pub use signet_cold_sql::SqlColdBackend; diff --git a/crates/storage/src/unified.rs b/crates/storage/src/unified.rs index f6bc1cc..7312df3 100644 --- a/crates/storage/src/unified.rs +++ b/crates/storage/src/unified.rs @@ -105,6 +105,11 @@ impl UnifiedStorage { &self.hot } + /// Consume self and return the hot storage backend. + pub fn into_hot(self) -> H { + self.hot + } + /// Get a reference to the cold storage handle. pub const fn cold(&self) -> &ColdStorageHandle { &self.cold