//! Configuration types for distributed storage backends use serde::{Deserialize, Serialize}; /// Redundancy strategy for object storage #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(tag = "type", rename_all = "snake_case")] pub enum RedundancyMode { /// No redundancy (local storage only) None, /// Reed-Solomon erasure coding ErasureCoded { /// Number of data shards data_shards: usize, /// Number of parity shards parity_shards: usize, }, /// Simple N-way replication Replicated { /// Number of replicas (including primary) replica_count: usize, /// Read quorum (minimum replicas for successful read) read_quorum: usize, /// Write quorum (minimum replicas for successful write) write_quorum: usize, }, } impl Default for RedundancyMode { fn default() -> Self { // Default: 4+2 erasure coding (1.5x overhead, tolerates 2 failures) Self::ErasureCoded { data_shards: 4, parity_shards: 2, } } } impl RedundancyMode { /// Create a new erasure coded configuration pub fn erasure_coded(data_shards: usize, parity_shards: usize) -> Self { Self::ErasureCoded { data_shards, parity_shards, } } /// Create a new replicated configuration with default quorums pub fn replicated(replica_count: usize) -> Self { Self::Replicated { replica_count, read_quorum: 1, write_quorum: (replica_count / 2) + 1, } } /// Create a new replicated configuration with custom quorums pub fn replicated_with_quorum( replica_count: usize, read_quorum: usize, write_quorum: usize, ) -> Self { Self::Replicated { replica_count, read_quorum, write_quorum, } } /// Get the minimum number of nodes required for this redundancy mode pub fn min_nodes(&self) -> usize { match self { Self::None => 1, Self::ErasureCoded { data_shards, parity_shards, } => data_shards + parity_shards, Self::Replicated { replica_count, .. } => *replica_count, } } /// Get the storage overhead factor (1.0 = no overhead) pub fn overhead_factor(&self) -> f64 { match self { Self::None => 1.0, Self::ErasureCoded { data_shards, parity_shards, } => (*data_shards + *parity_shards) as f64 / *data_shards as f64, Self::Replicated { replica_count, .. } => *replica_count as f64, } } /// Get the number of node failures that can be tolerated pub fn fault_tolerance(&self) -> usize { match self { Self::None => 0, Self::ErasureCoded { parity_shards, .. } => *parity_shards, Self::Replicated { replica_count, write_quorum, .. } => replica_count - write_quorum, } } } /// Chunk size configuration #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ChunkConfig { /// Default chunk size in bytes (default: 8 MiB) #[serde(default = "ChunkConfig::default_chunk_size")] pub chunk_size: usize, /// Minimum chunk size in bytes (default: 1 MiB) #[serde(default = "ChunkConfig::default_min_chunk_size")] pub min_chunk_size: usize, /// Maximum chunk size in bytes (default: 64 MiB) #[serde(default = "ChunkConfig::default_max_chunk_size")] pub max_chunk_size: usize, } impl ChunkConfig { const fn default_chunk_size() -> usize { 8 * 1024 * 1024 // 8 MiB } const fn default_min_chunk_size() -> usize { 1024 * 1024 // 1 MiB } const fn default_max_chunk_size() -> usize { 64 * 1024 * 1024 // 64 MiB } /// Create a new chunk configuration with custom chunk size pub fn new(chunk_size: usize) -> Self { Self { chunk_size, min_chunk_size: Self::default_min_chunk_size(), max_chunk_size: Self::default_max_chunk_size(), } } } impl Default for ChunkConfig { fn default() -> Self { Self { chunk_size: Self::default_chunk_size(), min_chunk_size: Self::default_min_chunk_size(), max_chunk_size: Self::default_max_chunk_size(), } } } /// Distributed storage configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DistributedConfig { /// Redundancy mode #[serde(default)] pub redundancy: RedundancyMode, /// Chunk configuration #[serde(default)] pub chunk: ChunkConfig, /// Node endpoints (for static configuration) #[serde(default)] pub node_endpoints: Vec, /// Registry endpoint (for dynamic discovery via ChainFire) pub registry_endpoint: Option, /// Connection timeout in milliseconds #[serde(default = "DistributedConfig::default_connection_timeout")] pub connection_timeout_ms: u64, /// Request timeout in milliseconds #[serde(default = "DistributedConfig::default_request_timeout")] pub request_timeout_ms: u64, /// Maximum retries for failed operations #[serde(default = "DistributedConfig::default_max_retries")] pub max_retries: u32, } impl DistributedConfig { const fn default_connection_timeout() -> u64 { 5000 // 5 seconds } const fn default_request_timeout() -> u64 { 300000 // 5 minutes } const fn default_max_retries() -> u32 { 3 } } impl Default for DistributedConfig { fn default() -> Self { Self { redundancy: RedundancyMode::default(), chunk: ChunkConfig::default(), node_endpoints: vec![], registry_endpoint: None, connection_timeout_ms: Self::default_connection_timeout(), request_timeout_ms: Self::default_request_timeout(), max_retries: Self::default_max_retries(), } } } /// Bucket-level storage configuration override #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct BucketStorageConfig { /// Override redundancy mode for this bucket pub redundancy: Option, /// Override chunk size for this bucket pub chunk_size: Option, } #[cfg(test)] mod tests { use super::*; #[test] fn test_redundancy_mode_default() { let mode = RedundancyMode::default(); assert!(matches!( mode, RedundancyMode::ErasureCoded { data_shards: 4, parity_shards: 2 } )); } #[test] fn test_redundancy_mode_min_nodes() { assert_eq!(RedundancyMode::None.min_nodes(), 1); assert_eq!(RedundancyMode::erasure_coded(4, 2).min_nodes(), 6); assert_eq!(RedundancyMode::replicated(3).min_nodes(), 3); } #[test] fn test_redundancy_mode_overhead() { assert!((RedundancyMode::None.overhead_factor() - 1.0).abs() < f64::EPSILON); assert!((RedundancyMode::erasure_coded(4, 2).overhead_factor() - 1.5).abs() < f64::EPSILON); assert!((RedundancyMode::replicated(3).overhead_factor() - 3.0).abs() < f64::EPSILON); } #[test] fn test_redundancy_mode_fault_tolerance() { assert_eq!(RedundancyMode::None.fault_tolerance(), 0); assert_eq!(RedundancyMode::erasure_coded(4, 2).fault_tolerance(), 2); // replica_count=3, write_quorum=2 -> can tolerate 1 failure assert_eq!(RedundancyMode::replicated(3).fault_tolerance(), 1); } #[test] fn test_chunk_config_default() { let config = ChunkConfig::default(); assert_eq!(config.chunk_size, 8 * 1024 * 1024); assert_eq!(config.min_chunk_size, 1024 * 1024); assert_eq!(config.max_chunk_size, 64 * 1024 * 1024); } #[test] fn test_distributed_config_default() { let config = DistributedConfig::default(); assert!(matches!( config.redundancy, RedundancyMode::ErasureCoded { .. } )); assert!(config.node_endpoints.is_empty()); assert!(config.registry_endpoint.is_none()); } #[test] fn test_redundancy_mode_serialization() { let ec = RedundancyMode::erasure_coded(4, 2); let json = serde_json::to_string(&ec).unwrap(); let parsed: RedundancyMode = serde_json::from_str(&json).unwrap(); assert_eq!(ec, parsed); let rep = RedundancyMode::replicated(3); let json = serde_json::to_string(&rep).unwrap(); let parsed: RedundancyMode = serde_json::from_str(&json).unwrap(); assert_eq!(rep, parsed); } }