From 38bfece6b99ab015a6932568a9184cfd906471d7 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 16 Feb 2026 17:03:18 -0500 Subject: [PATCH 1/6] feat(storage): add configuration and builder for flexible storage instantiation Add configuration layer to signet-storage crate enabling three deployment modes: - Hot-only (MDBX only, no cold storage) - Hot + Cold MDBX (both hot and cold use MDBX) - Hot MDBX + Cold SQL (hot uses MDBX, cold uses PostgreSQL/SQLite) Changes: - Add config module with StorageMode enum and environment variable parsing - Add builder module with StorageBuilder and StorageInstance types - Add StorageInstance enum to safely distinguish hot-only vs unified storage - Update error types to support configuration and backend errors - Add factory methods and into_hot() to UnifiedStorage - Add sql and sqlite features for PostgreSQL and SQLite support - Add comprehensive unit tests for configuration and builder The implementation is fully backward compatible with existing code. Co-Authored-By: Claude Sonnet 4.5 --- crates/storage/Cargo.toml | 5 + crates/storage/src/builder.rs | 476 ++++++++++++++++++++++++++++++++++ crates/storage/src/config.rs | 209 +++++++++++++++ crates/storage/src/error.rs | 22 ++ crates/storage/src/lib.rs | 9 + crates/storage/src/unified.rs | 5 + 6 files changed, 726 insertions(+) create mode 100644 crates/storage/src/builder.rs create mode 100644 crates/storage/src/config.rs 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 From e22517fe13acd112f3b2095d52a7980b781d9dac Mon Sep 17 00:00:00 2001 From: James Date: Mon, 16 Feb 2026 17:06:48 -0500 Subject: [PATCH 2/6] chore: bump version to 0.6.4 Co-Authored-By: Claude Sonnet 4.5 --- Cargo.toml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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" } From 43282bb64765961a39ec95d3576717fa3c304c84 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 16 Feb 2026 21:55:33 -0500 Subject: [PATCH 3/6] refactor(storage): introduce connector-based architecture Replace mode-based builder with trait-based connectors to decouple backend instantiation. Adds HotConnect and ColdConnect traits, unified MdbxConnector implementing both, and SqlConnector for auto-detecting PostgreSQL/SQLite. Removes StorageMode enum and StorageInstance enum, simplifying the API to always return UnifiedStorage. Users now pass connector objects to the builder's fluent API (.hot().cold().build()), enabling flexible backend composition without tight coupling. Co-Authored-By: Claude Opus 4.6 --- crates/cold-mdbx/src/connector.rs | 99 ++++++ crates/cold-mdbx/src/lib.rs | 3 + crates/cold-sql/src/connector.rs | 72 ++++ crates/cold-sql/src/error.rs | 4 + crates/cold-sql/src/lib.rs | 5 + crates/cold/src/connect.rs | 21 ++ crates/cold/src/lib.rs | 3 + crates/hot-mdbx/src/error.rs | 4 + crates/hot/src/connect.rs | 20 ++ crates/hot/src/lib.rs | 3 + crates/storage/Cargo.toml | 2 +- crates/storage/src/builder.rs | 545 ++++++++---------------------- crates/storage/src/config.rs | 202 ++--------- crates/storage/src/either.rs | 286 ++++++++++++++++ crates/storage/src/error.rs | 2 +- crates/storage/src/lib.rs | 15 +- 16 files changed, 705 insertions(+), 581 deletions(-) create mode 100644 crates/cold-mdbx/src/connector.rs create mode 100644 crates/cold-sql/src/connector.rs create mode 100644 crates/cold/src/connect.rs create mode 100644 crates/hot/src/connect.rs create mode 100644 crates/storage/src/either.rs diff --git a/crates/cold-mdbx/src/connector.rs b/crates/cold-mdbx/src/connector.rs new file mode 100644 index 0000000..aa7e71a --- /dev/null +++ b/crates/cold-mdbx/src/connector.rs @@ -0,0 +1,99 @@ +//! MDBX storage connector. +//! +//! Unified connector that can open both hot and cold MDBX databases. + +use crate::{MdbxColdBackend, MdbxColdError}; +use signet_cold::ColdConnect; +use signet_hot::HotConnect; +use signet_hot_mdbx::{DatabaseArguments, DatabaseEnv, MdbxError}; +use std::path::PathBuf; + +/// Connector for MDBX storage (both hot and cold). +/// +/// This unified connector can open MDBX databases for both hot and cold storage. +/// It holds the path and database arguments, which can include custom geometry, +/// sync mode, max readers, and other MDBX-specific configuration. +/// +/// # Example +/// +/// ```ignore +/// use signet_hot_mdbx::{MdbxConnector, DatabaseArguments}; +/// +/// // Hot storage with custom args +/// let hot = MdbxConnector::new("/tmp/hot") +/// .with_db_args(DatabaseArguments::new().with_max_readers(1000)); +/// +/// // Cold storage with default args +/// let cold = MdbxConnector::new("/tmp/cold"); +/// ``` +#[derive(Debug, Clone)] +pub struct MdbxConnector { + path: PathBuf, + db_args: DatabaseArguments, +} + +impl MdbxConnector { + /// Create a new MDBX connector with default database arguments. + pub fn new(path: impl Into) -> Self { + Self { path: path.into(), db_args: DatabaseArguments::new() } + } + + /// Set custom database arguments. + /// + /// This allows configuring MDBX-specific settings like geometry, sync mode, + /// max readers, and exclusive mode. + #[must_use] + pub const fn with_db_args(mut self, db_args: DatabaseArguments) -> Self { + self.db_args = db_args; + self + } + + /// Get a reference to the path. + pub fn path(&self) -> &std::path::Path { + &self.path + } + + /// Get a reference to the database arguments. + pub const fn db_args(&self) -> &DatabaseArguments { + &self.db_args + } + + /// Create a connector from environment variables. + /// + /// Reads the path from the specified environment variable. + /// + /// # Example + /// + /// ```ignore + /// use signet_hot_mdbx::MdbxConnector; + /// + /// let hot = MdbxConnector::from_env("SIGNET_HOT_PATH")?; + /// let cold = MdbxConnector::from_env("SIGNET_COLD_PATH")?; + /// ``` + pub fn from_env(env_var: &str) -> Result { + let path: PathBuf = std::env::var(env_var) + .map_err(|_| MdbxError::Config(format!("missing environment variable: {env_var}")))? + .into(); + Ok(Self::new(path)) + } +} + +impl HotConnect for MdbxConnector { + type Hot = DatabaseEnv; + type Error = MdbxError; + + fn connect(&self) -> Result { + self.db_args.clone().open_rw(&self.path) + } +} + +impl ColdConnect for MdbxConnector { + type Cold = MdbxColdBackend; + type Error = MdbxColdError; + + async fn connect(&self) -> Result { + // MDBX open is sync, but wrapped in async for trait consistency + // Opens read-write and creates tables + MdbxColdBackend::open_rw(&self.path) + } +} diff --git a/crates/cold-mdbx/src/lib.rs b/crates/cold-mdbx/src/lib.rs index 09f1de6..fad49ca 100644 --- a/crates/cold-mdbx/src/lib.rs +++ b/crates/cold-mdbx/src/lib.rs @@ -46,4 +46,7 @@ pub use tables::{ mod backend; pub use backend::MdbxColdBackend; +mod connector; +pub use connector::MdbxConnector; + pub use signet_hot_mdbx::{DatabaseArguments, DatabaseEnvKind}; diff --git a/crates/cold-sql/src/connector.rs b/crates/cold-sql/src/connector.rs new file mode 100644 index 0000000..2a0fe10 --- /dev/null +++ b/crates/cold-sql/src/connector.rs @@ -0,0 +1,72 @@ +//! SQL cold storage connector. + +use crate::{SqlColdBackend, SqlColdError}; +use signet_cold::ColdConnect; + +/// Connector for SQL cold storage (PostgreSQL or SQLite). +/// +/// Automatically detects the database type from the URL: +/// - URLs starting with `postgres://` or `postgresql://` use PostgreSQL +/// - URLs starting with `sqlite:` use SQLite +/// +/// # Example +/// +/// ```ignore +/// use signet_cold_sql::SqlConnector; +/// +/// // PostgreSQL +/// let pg = SqlConnector::new("postgres://localhost/signet"); +/// let backend = pg.connect().await?; +/// +/// // SQLite +/// let sqlite = SqlConnector::new("sqlite::memory:"); +/// let backend = sqlite.connect().await?; +/// ``` +#[cfg(any(feature = "sqlite", feature = "postgres"))] +#[derive(Debug, Clone)] +pub struct SqlConnector { + url: String, +} + +#[cfg(any(feature = "sqlite", feature = "postgres"))] +impl SqlConnector { + /// Create a new SQL connector. + /// + /// The database type is detected from the URL prefix. + pub fn new(url: impl Into) -> Self { + Self { url: url.into() } + } + + /// Get a reference to the connection URL. + pub fn url(&self) -> &str { + &self.url + } + + /// Create a connector from environment variables. + /// + /// Reads the SQL URL from the specified environment variable. + /// + /// # Example + /// + /// ```ignore + /// use signet_cold_sql::SqlConnector; + /// + /// let cold = SqlConnector::from_env("SIGNET_COLD_SQL_URL")?; + /// ``` + pub fn from_env(env_var: &str) -> Result { + let url = std::env::var(env_var).map_err(|_| { + SqlColdError::Config(format!("missing environment variable: {env_var}")) + })?; + Ok(Self::new(url)) + } +} + +#[cfg(any(feature = "sqlite", feature = "postgres"))] +impl ColdConnect for SqlConnector { + type Cold = SqlColdBackend; + type Error = SqlColdError; + + async fn connect(&self) -> Result { + SqlColdBackend::connect(&self.url).await + } +} diff --git a/crates/cold-sql/src/error.rs b/crates/cold-sql/src/error.rs index 00e1633..0b7179c 100644 --- a/crates/cold-sql/src/error.rs +++ b/crates/cold-sql/src/error.rs @@ -10,6 +10,10 @@ pub enum SqlColdError { /// A data conversion error occurred. #[error("conversion error: {0}")] Convert(String), + + /// Configuration error. + #[error("configuration error: {0}")] + Config(String), } impl From for signet_cold::ColdStorageError { diff --git a/crates/cold-sql/src/lib.rs b/crates/cold-sql/src/lib.rs index 4a2e7cf..dcb4c89 100644 --- a/crates/cold-sql/src/lib.rs +++ b/crates/cold-sql/src/lib.rs @@ -47,6 +47,11 @@ mod backend; #[cfg(any(feature = "sqlite", feature = "postgres"))] pub use backend::SqlColdBackend; +#[cfg(any(feature = "sqlite", feature = "postgres"))] +mod connector; +#[cfg(any(feature = "sqlite", feature = "postgres"))] +pub use connector::SqlConnector; + /// Backward-compatible alias for [`SqlColdBackend`] when using SQLite. #[cfg(feature = "sqlite")] pub type SqliteColdBackend = SqlColdBackend; diff --git a/crates/cold/src/connect.rs b/crates/cold/src/connect.rs new file mode 100644 index 0000000..b28efc7 --- /dev/null +++ b/crates/cold/src/connect.rs @@ -0,0 +1,21 @@ +//! Connection traits for cold storage backends. + +use crate::ColdStorage; + +/// Connector trait for cold storage backends. +/// +/// Abstracts the connection/opening process for cold storage, allowing +/// different backends to implement their own initialization logic. +pub trait ColdConnect { + /// The cold storage type produced by this connector. + type Cold: ColdStorage; + + /// The error type returned by connection attempts. + type Error: std::error::Error + Send + Sync + 'static; + + /// Connect to the cold storage backend asynchronously. + /// + /// Async to support backends that require async initialization + /// (like SQL connection pools). + async fn connect(&self) -> Result; +} diff --git a/crates/cold/src/lib.rs b/crates/cold/src/lib.rs index ec843cd..866293c 100644 --- a/crates/cold/src/lib.rs +++ b/crates/cold/src/lib.rs @@ -161,6 +161,9 @@ pub use stream::{StreamParams, produce_log_stream_default}; mod traits; pub use traits::{BlockData, ColdStorage, LogStream}; +pub mod connect; +pub use connect::ColdConnect; + /// Task module containing the storage task runner and handles. pub mod task; pub use task::{ColdStorageHandle, ColdStorageReadHandle, ColdStorageTask}; diff --git a/crates/hot-mdbx/src/error.rs b/crates/hot-mdbx/src/error.rs index a2d2658..091d10c 100644 --- a/crates/hot-mdbx/src/error.rs +++ b/crates/hot-mdbx/src/error.rs @@ -55,6 +55,10 @@ pub enum MdbxError { /// Deser. #[error(transparent)] Deser(#[from] DeserError), + + /// Configuration error. + #[error("configuration error: {0}")] + Config(String), } impl trevm::revm::database::DBErrorMarker for MdbxError {} diff --git a/crates/hot/src/connect.rs b/crates/hot/src/connect.rs new file mode 100644 index 0000000..b1575c8 --- /dev/null +++ b/crates/hot/src/connect.rs @@ -0,0 +1,20 @@ +//! Connection traits for hot storage backends. + +use crate::model::HotKv; + +/// Connector trait for hot storage backends. +/// +/// Abstracts the connection/opening process for hot storage, allowing +/// different backends to implement their own initialization logic. +pub trait HotConnect { + /// The hot storage type produced by this connector. + type Hot: HotKv; + + /// The error type returned by connection attempts. + type Error: std::error::Error + Send + Sync + 'static; + + /// Connect to the hot storage backend. + /// + /// Synchronous since most hot storage backends use sync initialization. + fn connect(&self) -> Result; +} diff --git a/crates/hot/src/lib.rs b/crates/hot/src/lib.rs index 5bf1ab6..6430967 100644 --- a/crates/hot/src/lib.rs +++ b/crates/hot/src/lib.rs @@ -105,6 +105,9 @@ #[cfg(any(test, feature = "test-utils"))] pub mod conformance; +pub mod connect; +pub use connect::HotConnect; + pub mod db; pub use db::{HistoryError, HistoryRead, HistoryWrite}; diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml index 81c0e7c..fc8c8f2 100644 --- a/crates/storage/Cargo.toml +++ b/crates/storage/Cargo.toml @@ -36,5 +36,5 @@ trevm.workspace = true [features] default = [] test-utils = ["signet-hot/test-utils", "signet-cold/test-utils"] -sql = ["signet-cold-sql/postgres"] +postgres = ["signet-cold-sql/postgres"] sqlite = ["signet-cold-sql/sqlite"] diff --git a/crates/storage/src/builder.rs b/crates/storage/src/builder.rs index c446fc3..3c996ae 100644 --- a/crates/storage/src/builder.rs +++ b/crates/storage/src/builder.rs @@ -1,476 +1,217 @@ //! 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}, + config::{ConfigError, ENV_COLD_PATH, ENV_COLD_SQL_URL, ENV_HOT_PATH}, + either::Either, }; +use signet_cold::ColdConnect; +use signet_cold_mdbx::MdbxConnector; +use signet_hot::HotConnect; +use std::env; 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, - } - } +#[cfg(any(feature = "postgres", feature = "sqlite"))] +use signet_cold_sql::SqlConnector; - /// 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, - } - } +#[cfg(any(feature = "postgres", feature = "sqlite"))] +type EnvColdConnector = Either; - /// 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, - } - } -} +#[cfg(not(any(feature = "postgres", feature = "sqlite")))] +type EnvColdConnector = Either; -/// Builder for storage configuration. +/// Builder for unified storage configuration. +/// +/// Uses a fluent API with `hot()`, `cold()`, and `build()` methods. +/// +/// # Example /// -/// 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, +/// ```ignore +/// use signet_storage::StorageBuilder; +/// use signet_hot_mdbx::MdbxConnector; +/// +/// let hot = MdbxConnector::new("/tmp/hot"); +/// let cold = MdbxConnector::new("/tmp/cold"); +/// +/// let storage = StorageBuilder::default() +/// .hot(hot) +/// .cold(cold) +/// .build() +/// .await?; +/// ``` +#[derive(Default)] +pub struct StorageBuilder { + hot_connector: H, + cold_connector: C, cancel_token: Option, } -impl Default for StorageBuilder { - fn default() -> Self { - Self::new() +impl StorageBuilder<(), ()> { + /// Create a new empty storage builder. + pub fn new() -> Self { + Self::default() } } -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, +impl StorageBuilder { + /// Set the hot storage connector. + pub fn hot(self, hot_connector: NewH) -> StorageBuilder { + StorageBuilder { + hot_connector, + cold_connector: self.cold_connector, + cancel_token: self.cancel_token, } } - /// 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); - } + /// Set the cold storage connector. + pub fn cold(self, cold_connector: NewC) -> StorageBuilder { + StorageBuilder { + hot_connector: self.hot_connector, + cold_connector, + cancel_token: self.cancel_token, } - - 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. + /// Set the cancellation token for the cold storage task. #[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(); +impl StorageBuilder +where + H: HotConnect, + C: ColdConnect, +{ + /// Build the unified storage instance. + /// + /// Opens both hot and cold backends and spawns the cold storage task. + pub async fn build(self) -> StorageResult> { + // Connect to hot storage (sync) + let hot = self + .hot_connector + .connect() + .map_err(|e| StorageError::Config(format!("hot connection failed: {e}")))?; + + // Connect to cold storage (async) + let cold = self + .cold_connector + .connect() + .await + .map_err(|e| StorageError::Config(format!("cold connection failed: {e}")))?; + + // Use provided cancel token or create new one 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(), - )), - } + // Spawn unified storage with cold task + Ok(UnifiedStorage::spawn(hot, cold, cancel_token)) } +} - /// Build the storage instance asynchronously. - /// - /// Required for SQL-based cold storage which needs async initialization. - /// Works for all storage modes. +impl StorageBuilder { + /// Create a builder from environment variables. /// - /// # Errors + /// Reads configuration from: + /// - `SIGNET_HOT_PATH`: Hot storage path (required) + /// - `SIGNET_COLD_PATH`: Cold MDBX path (optional) + /// - `SIGNET_COLD_SQL_URL`: Cold SQL URL (optional, requires postgres or sqlite feature) /// - /// 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(), - ) + /// Checks for `SIGNET_COLD_PATH` first. If present, uses MDBX cold backend. + /// Otherwise checks for `SIGNET_COLD_SQL_URL` for SQL backend. + /// Exactly one cold backend must be specified. + pub fn from_env() -> Result { + // Hot connector from environment (always MDBX) + let hot_connector = MdbxConnector::from_env(ENV_HOT_PATH) + .map_err(|e| ConfigError::ConnectorError { connector: "hot", error: e.to_string() })?; + + // Determine cold backend from environment + let has_mdbx = env::var(ENV_COLD_PATH).is_ok(); + let has_sql = env::var(ENV_COLD_SQL_URL).is_ok(); + + let cold_connector = match (has_mdbx, has_sql) { + (true, false) => { + let mdbx = MdbxConnector::from_env(ENV_COLD_PATH).map_err(|e| { + ConfigError::ConnectorError { connector: "cold MDBX", error: e.to_string() } })?; - Self::build_hot_cold_mdbx_impl(&hot_path, &cold_path, db_args, cancel_token) + Either::left(mdbx) } - 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 + (false, true) => { + #[cfg(any(feature = "postgres", feature = "sqlite"))] + { + let sql = SqlConnector::from_env(ENV_COLD_SQL_URL).map_err(|e| { + ConfigError::ConnectorError { connector: "cold SQL", error: e.to_string() } + })?; + Either::right(sql) + } + #[cfg(not(any(feature = "postgres", feature = "sqlite")))] + { + return Err(ConfigError::FeatureNotEnabled { + feature: "postgres or sqlite", + env_var: ENV_COLD_SQL_URL, + }); + } } - } - } - - 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); + (true, true) => { + return Err(ConfigError::AmbiguousColdBackend); + } + (false, false) => { + return Err(ConfigError::MissingColdBackend); + } + }; - Ok(StorageInstance::Unified(unified)) + Ok(Self { hot_connector, cold_connector, cancel_token: None }) } } #[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 + // SAFETY: Test environment 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); - } + assert!(StorageBuilder::from_env().is_err()); } #[test] - fn from_env_hot_only_valid() { - // SAFETY: Test environment, single-threaded test execution + fn from_env_missing_cold_backend() { + // SAFETY: Test environment unsafe { - env::set_var(ENV_STORAGE_MODE, "hot-only"); env::set_var(ENV_HOT_PATH, "/tmp/hot"); + env::remove_var(ENV_COLD_PATH); + env::remove_var(ENV_COLD_SQL_URL); } - - 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); - } + let result = StorageBuilder::from_env(); + assert!(matches!(result, Err(ConfigError::MissingColdBackend))); } #[test] - fn from_env_hot_cold_mdbx_missing_cold_path() { - // SAFETY: Test environment, single-threaded test execution + fn from_env_ambiguous_cold_backend() { + // SAFETY: Test environment unsafe { - env::set_var(ENV_STORAGE_MODE, "hot-cold-mdbx"); env::set_var(ENV_HOT_PATH, "/tmp/hot"); - env::remove_var(ENV_COLD_PATH); + env::set_var(ENV_COLD_PATH, "/tmp/cold"); + env::set_var(ENV_COLD_SQL_URL, "postgres://localhost/db"); } - 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); - } + assert!(matches!(result, Err(ConfigError::AmbiguousColdBackend))); } #[test] - fn from_env_hot_cold_mdbx_valid() { - // SAFETY: Test environment, single-threaded test execution + fn from_env_mdbx_cold() { + // SAFETY: Test environment 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"); + env::remove_var(ENV_COLD_SQL_URL); } - 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); - } + // Verify it's an Either::Left + assert!(matches!(builder.cold_connector, Either::Left(_))); } } diff --git a/crates/storage/src/config.rs b/crates/storage/src/config.rs index 5182001..8574049 100644 --- a/crates/storage/src/config.rs +++ b/crates/storage/src/config.rs @@ -1,46 +1,20 @@ //! 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 +//! This module provides environment variable constants and error types +//! for storage configuration. //! //! # 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; +//! | `SIGNET_COLD_PATH` | Path to cold MDBX database | Cold backend is MDBX | +//! | `SIGNET_COLD_SQL_URL` | SQL connection string | Cold backend is SQL | //! -//! // 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"); } -//! ``` +//! Exactly one of `SIGNET_COLD_PATH` or `SIGNET_COLD_SQL_URL` must be set. -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"; @@ -50,72 +24,6 @@ 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 { @@ -123,87 +31,29 @@ pub enum ConfigError { #[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), + /// Cold backend not specified. + #[error("no cold backend specified: set either {ENV_COLD_PATH} or {ENV_COLD_SQL_URL}")] + MissingColdBackend, + + /// Multiple cold backends specified. + #[error("ambiguous cold backend: both {ENV_COLD_PATH} and {ENV_COLD_SQL_URL} are set")] + AmbiguousColdBackend, - /// 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. + /// Required feature not enabled. + #[error("feature '{feature}' required for {env_var} but not enabled")] + FeatureNotEnabled { + /// The feature name that is required. + feature: &'static str, + /// The environment variable that requires the feature. 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); - } - } + /// Connector initialization error. + #[error("{connector} connector error: {error}")] + ConnectorError { + /// The connector type (e.g., "hot", "cold MDBX", "cold SQL"). + connector: &'static str, + /// The underlying error message. + error: String, + }, } diff --git a/crates/storage/src/either.rs b/crates/storage/src/either.rs new file mode 100644 index 0000000..b048a58 --- /dev/null +++ b/crates/storage/src/either.rs @@ -0,0 +1,286 @@ +//! Either type for holding one of two connector types. + +use alloy::primitives::BlockNumber; +use signet_cold::{ + BlockData, ColdConnect, ColdReceipt, ColdResult, ColdStorage, Confirmed, Filter, + HeaderSpecifier, ReceiptSpecifier, SignetEventsSpecifier, StreamParams, TransactionSpecifier, + ZenithHeaderSpecifier, +}; +use signet_cold_mdbx::{MdbxColdBackend, MdbxConnector}; +use signet_storage_types::{DbSignetEvent, DbZenithHeader, RecoveredTx, SealedHeader}; +use std::future::Future; + +#[cfg(any(feature = "postgres", feature = "sqlite"))] +use signet_cold_sql::{SqlColdBackend, SqlConnector}; + +type RpcLog = alloy::rpc::types::Log; + +/// Either type that holds one of two cold connectors. +/// +/// Used by `from_env()` to support both MDBX and SQL cold backends. +#[derive(Debug, Clone)] +pub enum Either { + /// Left variant. + Left(L), + /// Right variant. + Right(R), +} + +impl Either { + /// Create a left variant. + pub const fn left(value: L) -> Self { + Self::Left(value) + } + + /// Create a right variant. + pub const fn right(value: R) -> Self { + Self::Right(value) + } +} + +/// Enum to hold either cold backend type. +#[derive(Debug)] +pub enum EitherCold { + /// MDBX cold backend. + Mdbx(MdbxColdBackend), + /// SQL cold backend (PostgreSQL or SQLite). + #[cfg(any(feature = "postgres", feature = "sqlite"))] + Sql(SqlColdBackend), +} + +// Implement ColdStorage for EitherCold by dispatching to inner type +impl ColdStorage for EitherCold { + fn get_header( + &self, + spec: HeaderSpecifier, + ) -> impl Future>> + Send { + async move { + match self { + Self::Mdbx(backend) => backend.get_header(spec).await, + #[cfg(any(feature = "postgres", feature = "sqlite"))] + Self::Sql(backend) => backend.get_header(spec).await, + } + } + } + + fn get_headers( + &self, + specs: Vec, + ) -> impl Future>>> + Send { + async move { + match self { + Self::Mdbx(backend) => backend.get_headers(specs).await, + #[cfg(any(feature = "postgres", feature = "sqlite"))] + Self::Sql(backend) => backend.get_headers(specs).await, + } + } + } + + fn get_transaction( + &self, + spec: TransactionSpecifier, + ) -> impl Future>>> + Send { + async move { + match self { + Self::Mdbx(backend) => backend.get_transaction(spec).await, + #[cfg(any(feature = "postgres", feature = "sqlite"))] + Self::Sql(backend) => backend.get_transaction(spec).await, + } + } + } + + fn get_transactions_in_block( + &self, + block: BlockNumber, + ) -> impl Future>> + Send { + async move { + match self { + Self::Mdbx(backend) => backend.get_transactions_in_block(block).await, + #[cfg(any(feature = "postgres", feature = "sqlite"))] + Self::Sql(backend) => backend.get_transactions_in_block(block).await, + } + } + } + + fn get_transaction_count( + &self, + block: BlockNumber, + ) -> impl Future> + Send { + async move { + match self { + Self::Mdbx(backend) => backend.get_transaction_count(block).await, + #[cfg(any(feature = "postgres", feature = "sqlite"))] + Self::Sql(backend) => backend.get_transaction_count(block).await, + } + } + } + + fn get_receipt( + &self, + spec: ReceiptSpecifier, + ) -> impl Future>> + Send { + async move { + match self { + Self::Mdbx(backend) => backend.get_receipt(spec).await, + #[cfg(any(feature = "postgres", feature = "sqlite"))] + Self::Sql(backend) => backend.get_receipt(spec).await, + } + } + } + + fn get_receipts_in_block( + &self, + block: BlockNumber, + ) -> impl Future>> + Send { + async move { + match self { + Self::Mdbx(backend) => backend.get_receipts_in_block(block).await, + #[cfg(any(feature = "postgres", feature = "sqlite"))] + Self::Sql(backend) => backend.get_receipts_in_block(block).await, + } + } + } + + fn get_signet_events( + &self, + spec: SignetEventsSpecifier, + ) -> impl Future>> + Send { + async move { + match self { + Self::Mdbx(backend) => backend.get_signet_events(spec).await, + #[cfg(any(feature = "postgres", feature = "sqlite"))] + Self::Sql(backend) => backend.get_signet_events(spec).await, + } + } + } + + fn get_zenith_header( + &self, + spec: ZenithHeaderSpecifier, + ) -> impl Future>> + Send { + async move { + match self { + Self::Mdbx(backend) => backend.get_zenith_header(spec).await, + #[cfg(any(feature = "postgres", feature = "sqlite"))] + Self::Sql(backend) => backend.get_zenith_header(spec).await, + } + } + } + + fn get_zenith_headers( + &self, + spec: ZenithHeaderSpecifier, + ) -> impl Future>> + Send { + async move { + match self { + Self::Mdbx(backend) => backend.get_zenith_headers(spec).await, + #[cfg(any(feature = "postgres", feature = "sqlite"))] + Self::Sql(backend) => backend.get_zenith_headers(spec).await, + } + } + } + + fn get_latest_block(&self) -> impl Future>> + Send { + async move { + match self { + Self::Mdbx(backend) => backend.get_latest_block().await, + #[cfg(any(feature = "postgres", feature = "sqlite"))] + Self::Sql(backend) => backend.get_latest_block().await, + } + } + } + + fn get_logs( + &self, + filter: &Filter, + max_logs: usize, + ) -> impl Future>> + Send { + async move { + match self { + Self::Mdbx(backend) => backend.get_logs(filter, max_logs).await, + #[cfg(any(feature = "postgres", feature = "sqlite"))] + Self::Sql(backend) => backend.get_logs(filter, max_logs).await, + } + } + } + + fn produce_log_stream( + &self, + filter: &Filter, + params: StreamParams, + ) -> impl Future + Send { + async move { + match self { + Self::Mdbx(backend) => backend.produce_log_stream(filter, params).await, + #[cfg(any(feature = "postgres", feature = "sqlite"))] + Self::Sql(backend) => backend.produce_log_stream(filter, params).await, + } + } + } + + fn append_block(&self, data: BlockData) -> impl Future> + Send { + async move { + match self { + Self::Mdbx(backend) => backend.append_block(data).await, + #[cfg(any(feature = "postgres", feature = "sqlite"))] + Self::Sql(backend) => backend.append_block(data).await, + } + } + } + + fn append_blocks(&self, data: Vec) -> impl Future> + Send { + async move { + match self { + Self::Mdbx(backend) => backend.append_blocks(data).await, + #[cfg(any(feature = "postgres", feature = "sqlite"))] + Self::Sql(backend) => backend.append_blocks(data).await, + } + } + } + + fn truncate_above(&self, block: BlockNumber) -> impl Future> + Send { + async move { + match self { + Self::Mdbx(backend) => backend.truncate_above(block).await, + #[cfg(any(feature = "postgres", feature = "sqlite"))] + Self::Sql(backend) => backend.truncate_above(block).await, + } + } + } +} + +// When SQL features are enabled +#[cfg(any(feature = "postgres", feature = "sqlite"))] +impl ColdConnect for Either { + type Cold = EitherCold; + type Error = crate::StorageError; + + async fn connect(&self) -> Result { + match self { + Either::Left(mdbx) => { + let backend = mdbx.connect().await.map_err(|e| crate::StorageError::MdbxCold(e))?; + Ok(EitherCold::Mdbx(backend)) + } + Either::Right(sql) => { + let backend = sql.connect().await.map_err(|e| crate::StorageError::SqlCold(e))?; + Ok(EitherCold::Sql(backend)) + } + } + } +} + +// Fallback for when no SQL features are enabled +#[cfg(not(any(feature = "postgres", feature = "sqlite")))] +impl ColdConnect for Either { + type Cold = MdbxColdBackend; + type Error = crate::StorageError; + + async fn connect(&self) -> Result { + match self { + Either::Left(mdbx) => { + mdbx.connect().await.map_err(|e| crate::StorageError::MdbxCold(e)) + } + Either::Right(()) => unreachable!("SQL not enabled"), + } + } +} diff --git a/crates/storage/src/error.rs b/crates/storage/src/error.rs index 44e2c3c..19a02eb 100644 --- a/crates/storage/src/error.rs +++ b/crates/storage/src/error.rs @@ -25,7 +25,7 @@ pub enum StorageError { #[error("MDBX cold storage error: {0}")] MdbxCold(#[from] MdbxColdError), /// SQL cold storage error. - #[cfg(feature = "sql")] + #[cfg(any(feature = "postgres", feature = "sqlite"))] #[error("SQL cold storage error: {0}")] SqlCold(#[from] signet_cold_sql::SqlColdError), } diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index 60a0113..a7b1116 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -64,9 +64,22 @@ pub mod config; pub mod builder; +pub mod either; +pub use either::Either; + mod unified; pub use unified::UnifiedStorage; +// Re-export connector traits +pub use signet_cold::ColdConnect; +pub use signet_hot::HotConnect; + +// Re-export unified connectors +pub use signet_cold_mdbx::MdbxConnector; + +#[cfg(any(feature = "postgres", feature = "sqlite"))] +pub use signet_cold_sql::SqlConnector; + // Re-export key types for convenience pub use signet_cold::{ColdStorage, ColdStorageError, ColdStorageHandle, ColdStorageTask}; pub use signet_cold_mdbx::MdbxColdBackend; @@ -78,5 +91,5 @@ pub use signet_hot_mdbx::{DatabaseArguments, DatabaseEnv}; pub use signet_storage_types::{ExecutedBlock, ExecutedBlockBuilder}; pub use tokio_util::sync::CancellationToken; -#[cfg(feature = "sql")] +#[cfg(any(feature = "postgres", feature = "sqlite"))] pub use signet_cold_sql::SqlColdBackend; From 868d8e1c57dc75e35bea23f925cd2d6d0ac82717 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 16 Feb 2026 22:03:46 -0500 Subject: [PATCH 4/6] refactor(storage): simplify EitherCold with dispatch_async! macro Reduces EitherCold boilerplate from 286 lines to 212 lines by introducing a dispatch_async! macro that generates the repetitive match-and-forward pattern for all 16 ColdStorage trait methods. Method signatures remain explicit for documentation while the async match blocks are generated by the macro, maintaining zero runtime overhead. Co-Authored-By: Claude Opus 4.6 --- crates/storage/src/either.rs | 150 +++++++++-------------------------- 1 file changed, 38 insertions(+), 112 deletions(-) diff --git a/crates/storage/src/either.rs b/crates/storage/src/either.rs index b048a58..c232b8c 100644 --- a/crates/storage/src/either.rs +++ b/crates/storage/src/either.rs @@ -1,4 +1,9 @@ //! Either type for holding one of two connector types. +//! +//! The `Either` type enables runtime backend selection while maintaining compile-time +//! type safety and zero-cost abstraction. The `dispatch_async!` macro reduces +//! boilerplate for the `EitherCold` implementation by generating the repetitive +//! match-and-forward pattern for all ColdStorage trait methods. use alloy::primitives::BlockNumber; use signet_cold::{ @@ -48,146 +53,97 @@ pub enum EitherCold { Sql(SqlColdBackend), } +/// Dispatches an async method call to the inner cold storage backend. +/// +/// This macro reduces boilerplate for EitherCold by generating the match-and-forward +/// pattern. It preserves the method signatures for clarity while eliminating the +/// repetitive async match blocks. +macro_rules! dispatch_async { + ($self:expr, $method:ident($($param:expr),*)) => { + async move { + match $self { + Self::Mdbx(backend) => backend.$method($($param),*).await, + #[cfg(any(feature = "postgres", feature = "sqlite"))] + Self::Sql(backend) => backend.$method($($param),*).await, + } + } + }; +} + // Implement ColdStorage for EitherCold by dispatching to inner type impl ColdStorage for EitherCold { fn get_header( &self, spec: HeaderSpecifier, ) -> impl Future>> + Send { - async move { - match self { - Self::Mdbx(backend) => backend.get_header(spec).await, - #[cfg(any(feature = "postgres", feature = "sqlite"))] - Self::Sql(backend) => backend.get_header(spec).await, - } - } + dispatch_async!(self, get_header(spec)) } fn get_headers( &self, specs: Vec, ) -> impl Future>>> + Send { - async move { - match self { - Self::Mdbx(backend) => backend.get_headers(specs).await, - #[cfg(any(feature = "postgres", feature = "sqlite"))] - Self::Sql(backend) => backend.get_headers(specs).await, - } - } + dispatch_async!(self, get_headers(specs)) } fn get_transaction( &self, spec: TransactionSpecifier, ) -> impl Future>>> + Send { - async move { - match self { - Self::Mdbx(backend) => backend.get_transaction(spec).await, - #[cfg(any(feature = "postgres", feature = "sqlite"))] - Self::Sql(backend) => backend.get_transaction(spec).await, - } - } + dispatch_async!(self, get_transaction(spec)) } fn get_transactions_in_block( &self, block: BlockNumber, ) -> impl Future>> + Send { - async move { - match self { - Self::Mdbx(backend) => backend.get_transactions_in_block(block).await, - #[cfg(any(feature = "postgres", feature = "sqlite"))] - Self::Sql(backend) => backend.get_transactions_in_block(block).await, - } - } + dispatch_async!(self, get_transactions_in_block(block)) } fn get_transaction_count( &self, block: BlockNumber, ) -> impl Future> + Send { - async move { - match self { - Self::Mdbx(backend) => backend.get_transaction_count(block).await, - #[cfg(any(feature = "postgres", feature = "sqlite"))] - Self::Sql(backend) => backend.get_transaction_count(block).await, - } - } + dispatch_async!(self, get_transaction_count(block)) } fn get_receipt( &self, spec: ReceiptSpecifier, ) -> impl Future>> + Send { - async move { - match self { - Self::Mdbx(backend) => backend.get_receipt(spec).await, - #[cfg(any(feature = "postgres", feature = "sqlite"))] - Self::Sql(backend) => backend.get_receipt(spec).await, - } - } + dispatch_async!(self, get_receipt(spec)) } fn get_receipts_in_block( &self, block: BlockNumber, ) -> impl Future>> + Send { - async move { - match self { - Self::Mdbx(backend) => backend.get_receipts_in_block(block).await, - #[cfg(any(feature = "postgres", feature = "sqlite"))] - Self::Sql(backend) => backend.get_receipts_in_block(block).await, - } - } + dispatch_async!(self, get_receipts_in_block(block)) } fn get_signet_events( &self, spec: SignetEventsSpecifier, ) -> impl Future>> + Send { - async move { - match self { - Self::Mdbx(backend) => backend.get_signet_events(spec).await, - #[cfg(any(feature = "postgres", feature = "sqlite"))] - Self::Sql(backend) => backend.get_signet_events(spec).await, - } - } + dispatch_async!(self, get_signet_events(spec)) } fn get_zenith_header( &self, spec: ZenithHeaderSpecifier, ) -> impl Future>> + Send { - async move { - match self { - Self::Mdbx(backend) => backend.get_zenith_header(spec).await, - #[cfg(any(feature = "postgres", feature = "sqlite"))] - Self::Sql(backend) => backend.get_zenith_header(spec).await, - } - } + dispatch_async!(self, get_zenith_header(spec)) } fn get_zenith_headers( &self, spec: ZenithHeaderSpecifier, ) -> impl Future>> + Send { - async move { - match self { - Self::Mdbx(backend) => backend.get_zenith_headers(spec).await, - #[cfg(any(feature = "postgres", feature = "sqlite"))] - Self::Sql(backend) => backend.get_zenith_headers(spec).await, - } - } + dispatch_async!(self, get_zenith_headers(spec)) } fn get_latest_block(&self) -> impl Future>> + Send { - async move { - match self { - Self::Mdbx(backend) => backend.get_latest_block().await, - #[cfg(any(feature = "postgres", feature = "sqlite"))] - Self::Sql(backend) => backend.get_latest_block().await, - } - } + dispatch_async!(self, get_latest_block()) } fn get_logs( @@ -195,13 +151,7 @@ impl ColdStorage for EitherCold { filter: &Filter, max_logs: usize, ) -> impl Future>> + Send { - async move { - match self { - Self::Mdbx(backend) => backend.get_logs(filter, max_logs).await, - #[cfg(any(feature = "postgres", feature = "sqlite"))] - Self::Sql(backend) => backend.get_logs(filter, max_logs).await, - } - } + dispatch_async!(self, get_logs(filter, max_logs)) } fn produce_log_stream( @@ -209,43 +159,19 @@ impl ColdStorage for EitherCold { filter: &Filter, params: StreamParams, ) -> impl Future + Send { - async move { - match self { - Self::Mdbx(backend) => backend.produce_log_stream(filter, params).await, - #[cfg(any(feature = "postgres", feature = "sqlite"))] - Self::Sql(backend) => backend.produce_log_stream(filter, params).await, - } - } + dispatch_async!(self, produce_log_stream(filter, params)) } fn append_block(&self, data: BlockData) -> impl Future> + Send { - async move { - match self { - Self::Mdbx(backend) => backend.append_block(data).await, - #[cfg(any(feature = "postgres", feature = "sqlite"))] - Self::Sql(backend) => backend.append_block(data).await, - } - } + dispatch_async!(self, append_block(data)) } fn append_blocks(&self, data: Vec) -> impl Future> + Send { - async move { - match self { - Self::Mdbx(backend) => backend.append_blocks(data).await, - #[cfg(any(feature = "postgres", feature = "sqlite"))] - Self::Sql(backend) => backend.append_blocks(data).await, - } - } + dispatch_async!(self, append_blocks(data)) } fn truncate_above(&self, block: BlockNumber) -> impl Future> + Send { - async move { - match self { - Self::Mdbx(backend) => backend.truncate_above(block).await, - #[cfg(any(feature = "postgres", feature = "sqlite"))] - Self::Sql(backend) => backend.truncate_above(block).await, - } - } + dispatch_async!(self, truncate_above(block)) } } From a723304aa9dd62be7b6cb1f7b83b34b13963f20f Mon Sep 17 00:00:00 2001 From: James Date: Mon, 16 Feb 2026 22:07:00 -0500 Subject: [PATCH 5/6] refactor(storage): use connector-specific errors, remove Config variants Creates dedicated error types for connector initialization: - MdbxConnectorError in cold-mdbx crate - SqlConnectorError in cold-sql crate Removes Config(String) variants from MdbxError and SqlColdError, which were only used for from_env() methods. The new connector error types are more specific and properly handle missing environment variables. ConfigError now uses From implementations to convert from connector errors, simplifying error handling in the builder. Co-Authored-By: Claude Opus 4.6 --- crates/cold-mdbx/src/connector.rs | 25 ++++++++++++++++++++----- crates/cold-mdbx/src/lib.rs | 2 +- crates/cold-sql/src/connector.rs | 18 ++++++++++++++---- crates/cold-sql/src/error.rs | 4 ---- crates/cold-sql/src/lib.rs | 2 +- crates/hot-mdbx/src/error.rs | 4 ---- crates/storage/src/builder.rs | 11 +++-------- crates/storage/src/config.rs | 20 ++++++++++++-------- 8 files changed, 51 insertions(+), 35 deletions(-) diff --git a/crates/cold-mdbx/src/connector.rs b/crates/cold-mdbx/src/connector.rs index aa7e71a..b51dfb9 100644 --- a/crates/cold-mdbx/src/connector.rs +++ b/crates/cold-mdbx/src/connector.rs @@ -8,6 +8,22 @@ use signet_hot::HotConnect; use signet_hot_mdbx::{DatabaseArguments, DatabaseEnv, MdbxError}; use std::path::PathBuf; +/// Errors that can occur when initializing MDBX connectors. +#[derive(Debug, thiserror::Error)] +pub enum MdbxConnectorError { + /// Missing environment variable. + #[error("missing environment variable: {0}")] + MissingEnvVar(&'static str), + + /// Hot storage initialization failed. + #[error("hot storage initialization failed: {0}")] + HotInit(#[from] MdbxError), + + /// Cold storage initialization failed. + #[error("cold storage initialization failed: {0}")] + ColdInit(#[from] MdbxColdError), +} + /// Connector for MDBX storage (both hot and cold). /// /// This unified connector can open MDBX databases for both hot and cold storage. @@ -65,15 +81,14 @@ impl MdbxConnector { /// # Example /// /// ```ignore - /// use signet_hot_mdbx::MdbxConnector; + /// use signet_cold_mdbx::MdbxConnector; /// /// let hot = MdbxConnector::from_env("SIGNET_HOT_PATH")?; /// let cold = MdbxConnector::from_env("SIGNET_COLD_PATH")?; /// ``` - pub fn from_env(env_var: &str) -> Result { - let path: PathBuf = std::env::var(env_var) - .map_err(|_| MdbxError::Config(format!("missing environment variable: {env_var}")))? - .into(); + pub fn from_env(env_var: &'static str) -> Result { + let path: PathBuf = + std::env::var(env_var).map_err(|_| MdbxConnectorError::MissingEnvVar(env_var))?.into(); Ok(Self::new(path)) } } diff --git a/crates/cold-mdbx/src/lib.rs b/crates/cold-mdbx/src/lib.rs index fad49ca..a91fa8b 100644 --- a/crates/cold-mdbx/src/lib.rs +++ b/crates/cold-mdbx/src/lib.rs @@ -47,6 +47,6 @@ mod backend; pub use backend::MdbxColdBackend; mod connector; -pub use connector::MdbxConnector; +pub use connector::{MdbxConnector, MdbxConnectorError}; pub use signet_hot_mdbx::{DatabaseArguments, DatabaseEnvKind}; diff --git a/crates/cold-sql/src/connector.rs b/crates/cold-sql/src/connector.rs index 2a0fe10..7872306 100644 --- a/crates/cold-sql/src/connector.rs +++ b/crates/cold-sql/src/connector.rs @@ -3,6 +3,18 @@ use crate::{SqlColdBackend, SqlColdError}; use signet_cold::ColdConnect; +/// Errors that can occur when initializing SQL connectors. +#[derive(Debug, thiserror::Error)] +pub enum SqlConnectorError { + /// Missing environment variable. + #[error("missing environment variable: {0}")] + MissingEnvVar(&'static str), + + /// Cold storage initialization failed. + #[error("cold storage initialization failed: {0}")] + ColdInit(#[from] SqlColdError), +} + /// Connector for SQL cold storage (PostgreSQL or SQLite). /// /// Automatically detects the database type from the URL: @@ -53,10 +65,8 @@ impl SqlConnector { /// /// let cold = SqlConnector::from_env("SIGNET_COLD_SQL_URL")?; /// ``` - pub fn from_env(env_var: &str) -> Result { - let url = std::env::var(env_var).map_err(|_| { - SqlColdError::Config(format!("missing environment variable: {env_var}")) - })?; + pub fn from_env(env_var: &'static str) -> Result { + let url = std::env::var(env_var).map_err(|_| SqlConnectorError::MissingEnvVar(env_var))?; Ok(Self::new(url)) } } diff --git a/crates/cold-sql/src/error.rs b/crates/cold-sql/src/error.rs index 0b7179c..00e1633 100644 --- a/crates/cold-sql/src/error.rs +++ b/crates/cold-sql/src/error.rs @@ -10,10 +10,6 @@ pub enum SqlColdError { /// A data conversion error occurred. #[error("conversion error: {0}")] Convert(String), - - /// Configuration error. - #[error("configuration error: {0}")] - Config(String), } impl From for signet_cold::ColdStorageError { diff --git a/crates/cold-sql/src/lib.rs b/crates/cold-sql/src/lib.rs index dcb4c89..f28e2ad 100644 --- a/crates/cold-sql/src/lib.rs +++ b/crates/cold-sql/src/lib.rs @@ -50,7 +50,7 @@ pub use backend::SqlColdBackend; #[cfg(any(feature = "sqlite", feature = "postgres"))] mod connector; #[cfg(any(feature = "sqlite", feature = "postgres"))] -pub use connector::SqlConnector; +pub use connector::{SqlConnector, SqlConnectorError}; /// Backward-compatible alias for [`SqlColdBackend`] when using SQLite. #[cfg(feature = "sqlite")] diff --git a/crates/hot-mdbx/src/error.rs b/crates/hot-mdbx/src/error.rs index 091d10c..a2d2658 100644 --- a/crates/hot-mdbx/src/error.rs +++ b/crates/hot-mdbx/src/error.rs @@ -55,10 +55,6 @@ pub enum MdbxError { /// Deser. #[error(transparent)] Deser(#[from] DeserError), - - /// Configuration error. - #[error("configuration error: {0}")] - Config(String), } impl trevm::revm::database::DBErrorMarker for MdbxError {} diff --git a/crates/storage/src/builder.rs b/crates/storage/src/builder.rs index 3c996ae..0ff57c0 100644 --- a/crates/storage/src/builder.rs +++ b/crates/storage/src/builder.rs @@ -123,8 +123,7 @@ impl StorageBuilder { /// Exactly one cold backend must be specified. pub fn from_env() -> Result { // Hot connector from environment (always MDBX) - let hot_connector = MdbxConnector::from_env(ENV_HOT_PATH) - .map_err(|e| ConfigError::ConnectorError { connector: "hot", error: e.to_string() })?; + let hot_connector = MdbxConnector::from_env(ENV_HOT_PATH)?; // Determine cold backend from environment let has_mdbx = env::var(ENV_COLD_PATH).is_ok(); @@ -132,17 +131,13 @@ impl StorageBuilder { let cold_connector = match (has_mdbx, has_sql) { (true, false) => { - let mdbx = MdbxConnector::from_env(ENV_COLD_PATH).map_err(|e| { - ConfigError::ConnectorError { connector: "cold MDBX", error: e.to_string() } - })?; + let mdbx = MdbxConnector::from_env(ENV_COLD_PATH)?; Either::left(mdbx) } (false, true) => { #[cfg(any(feature = "postgres", feature = "sqlite"))] { - let sql = SqlConnector::from_env(ENV_COLD_SQL_URL).map_err(|e| { - ConfigError::ConnectorError { connector: "cold SQL", error: e.to_string() } - })?; + let sql = SqlConnector::from_env(ENV_COLD_SQL_URL)?; Either::right(sql) } #[cfg(not(any(feature = "postgres", feature = "sqlite")))] diff --git a/crates/storage/src/config.rs b/crates/storage/src/config.rs index 8574049..53bb029 100644 --- a/crates/storage/src/config.rs +++ b/crates/storage/src/config.rs @@ -13,8 +13,12 @@ //! //! Exactly one of `SIGNET_COLD_PATH` or `SIGNET_COLD_SQL_URL` must be set. +use signet_cold_mdbx::MdbxConnectorError; use thiserror::Error; +#[cfg(any(feature = "postgres", feature = "sqlite"))] +use signet_cold_sql::SqlConnectorError; + /// Environment variable name for hot storage path. pub const ENV_HOT_PATH: &str = "SIGNET_HOT_PATH"; @@ -48,12 +52,12 @@ pub enum ConfigError { env_var: &'static str, }, - /// Connector initialization error. - #[error("{connector} connector error: {error}")] - ConnectorError { - /// The connector type (e.g., "hot", "cold MDBX", "cold SQL"). - connector: &'static str, - /// The underlying error message. - error: String, - }, + /// MDBX connector error. + #[error("MDBX connector error: {0}")] + MdbxConnector(#[from] MdbxConnectorError), + + /// SQL connector error. + #[cfg(any(feature = "postgres", feature = "sqlite"))] + #[error("SQL connector error: {0}")] + SqlConnector(#[from] SqlConnectorError), } From 33fbb4b9bc2d95cac5dc29650c3624c95a685499 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 16 Feb 2026 22:11:52 -0500 Subject: [PATCH 6/6] fix(storage): desugar async fn in ColdConnect trait to specify Send bound Addresses async-fn-in-trait clippy lint by explicitly desugaring async fn to return impl Future + Send. This makes the Send bound explicit and allows callers to know the future can be sent across threads. Changes: - Desugar ColdConnect::connect to return impl Future + Send - Add #[allow(clippy::manual_async_fn)] to MDBX and EitherCold impls - Fix redundant closures in error mapping - Add Debug derive to StorageBuilder - Add serial_test to workspace for test isolation Co-Authored-By: Claude Opus 4.6 --- Cargo.toml | 1 + crates/cold-mdbx/src/connector.rs | 6 ++++-- crates/cold-sql/src/connector.rs | 5 +++-- crates/cold/src/connect.rs | 2 +- crates/storage/Cargo.toml | 1 + crates/storage/src/builder.rs | 7 ++++++- crates/storage/src/either.rs | 33 ++++++++++++++++++------------- 7 files changed, 35 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5a69b45..630fee8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,7 @@ parking_lot = "0.12.5" rand = "0.9.2" rayon = "1.10" serde = { version = "1.0.217", features = ["derive"] } +serial_test = "3.3" tempfile = "3.20.0" thiserror = "2.0.18" tokio = { version = "1.45.0", features = ["full"] } diff --git a/crates/cold-mdbx/src/connector.rs b/crates/cold-mdbx/src/connector.rs index b51dfb9..6b47e04 100644 --- a/crates/cold-mdbx/src/connector.rs +++ b/crates/cold-mdbx/src/connector.rs @@ -106,9 +106,11 @@ impl ColdConnect for MdbxConnector { type Cold = MdbxColdBackend; type Error = MdbxColdError; - async fn connect(&self) -> Result { + #[allow(clippy::manual_async_fn)] + fn connect(&self) -> impl std::future::Future> + Send { // MDBX open is sync, but wrapped in async for trait consistency // Opens read-write and creates tables - MdbxColdBackend::open_rw(&self.path) + let path = self.path.clone(); + async move { MdbxColdBackend::open_rw(&path) } } } diff --git a/crates/cold-sql/src/connector.rs b/crates/cold-sql/src/connector.rs index 7872306..08bce0f 100644 --- a/crates/cold-sql/src/connector.rs +++ b/crates/cold-sql/src/connector.rs @@ -76,7 +76,8 @@ impl ColdConnect for SqlConnector { type Cold = SqlColdBackend; type Error = SqlColdError; - async fn connect(&self) -> Result { - SqlColdBackend::connect(&self.url).await + fn connect(&self) -> impl std::future::Future> + Send { + let url = self.url.clone(); + async move { SqlColdBackend::connect(&url).await } } } diff --git a/crates/cold/src/connect.rs b/crates/cold/src/connect.rs index b28efc7..7ee2a05 100644 --- a/crates/cold/src/connect.rs +++ b/crates/cold/src/connect.rs @@ -17,5 +17,5 @@ pub trait ColdConnect { /// /// Async to support backends that require async initialization /// (like SQL connection pools). - async fn connect(&self) -> Result; + fn connect(&self) -> impl std::future::Future> + Send; } diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml index fc8c8f2..8ad9963 100644 --- a/crates/storage/Cargo.toml +++ b/crates/storage/Cargo.toml @@ -28,6 +28,7 @@ thiserror.workspace = true tokio-util.workspace = true [dev-dependencies] +serial_test.workspace = true signet-storage = { path = ".", features = ["test-utils"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } tokio-util.workspace = true diff --git a/crates/storage/src/builder.rs b/crates/storage/src/builder.rs index 0ff57c0..b13e336 100644 --- a/crates/storage/src/builder.rs +++ b/crates/storage/src/builder.rs @@ -39,7 +39,7 @@ type EnvColdConnector = Either; /// .build() /// .await?; /// ``` -#[derive(Default)] +#[derive(Default, Debug)] pub struct StorageBuilder { hot_connector: H, cold_connector: C, @@ -163,8 +163,10 @@ impl StorageBuilder { #[cfg(test)] mod tests { use super::*; + use serial_test::serial; #[test] + #[serial] fn from_env_missing_hot_path() { // SAFETY: Test environment unsafe { @@ -174,6 +176,7 @@ mod tests { } #[test] + #[serial] fn from_env_missing_cold_backend() { // SAFETY: Test environment unsafe { @@ -186,6 +189,7 @@ mod tests { } #[test] + #[serial] fn from_env_ambiguous_cold_backend() { // SAFETY: Test environment unsafe { @@ -198,6 +202,7 @@ mod tests { } #[test] + #[serial] fn from_env_mdbx_cold() { // SAFETY: Test environment unsafe { diff --git a/crates/storage/src/either.rs b/crates/storage/src/either.rs index c232b8c..4aa90c6 100644 --- a/crates/storage/src/either.rs +++ b/crates/storage/src/either.rs @@ -71,6 +71,7 @@ macro_rules! dispatch_async { } // Implement ColdStorage for EitherCold by dispatching to inner type +#[allow(clippy::manual_async_fn)] impl ColdStorage for EitherCold { fn get_header( &self, @@ -181,15 +182,18 @@ impl ColdConnect for Either { type Cold = EitherCold; type Error = crate::StorageError; - async fn connect(&self) -> Result { - match self { - Either::Left(mdbx) => { - let backend = mdbx.connect().await.map_err(|e| crate::StorageError::MdbxCold(e))?; - Ok(EitherCold::Mdbx(backend)) - } - Either::Right(sql) => { - let backend = sql.connect().await.map_err(|e| crate::StorageError::SqlCold(e))?; - Ok(EitherCold::Sql(backend)) + fn connect(&self) -> impl std::future::Future> + Send { + let self_clone = self.clone(); + async move { + match self_clone { + Either::Left(mdbx) => { + let backend = mdbx.connect().await.map_err(crate::StorageError::MdbxCold)?; + Ok(EitherCold::Mdbx(backend)) + } + Either::Right(sql) => { + let backend = sql.connect().await.map_err(crate::StorageError::SqlCold)?; + Ok(EitherCold::Sql(backend)) + } } } } @@ -201,12 +205,13 @@ impl ColdConnect for Either { type Cold = MdbxColdBackend; type Error = crate::StorageError; - async fn connect(&self) -> Result { - match self { - Either::Left(mdbx) => { - mdbx.connect().await.map_err(|e| crate::StorageError::MdbxCold(e)) + fn connect(&self) -> impl std::future::Future> + Send { + let self_clone = self.clone(); + async move { + match self_clone { + Either::Left(mdbx) => mdbx.connect().await.map_err(crate::StorageError::MdbxCold), + Either::Right(()) => unreachable!("SQL not enabled"), } - Either::Right(()) => unreachable!("SQL not enabled"), } } }