mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-13 08:36:54 +00:00
* fix(ecstore): add fallible erasure construction (cherry picked from commitbd148b20f7) * fix(ecstore): resolve storage parity per pool (cherry picked from commitc05c2cb24b) * fix(ecstore): keep carved per-pool parity core self-contained on main Fixups so the cherry-picked fallible-erasure + per-pool-parity core builds standalone on current main without the excluded scope-creep commits: - runtime/sources.rs: re-add backend_storage_class_parities (removed by the per-pool commit; its rebalance caller was updated in an unrelated reporting commit that was left out). Reimplemented over the snapshot API, behavior-identical. - config/mod.rs: rename the storage-class publish test module (main independently added a mod tests, so the cherry-pick collided). - rustfs storage_api.rs + startup_storage.rs: route the storage-class ENV consts through the startup storage facade and use a local const for the erasure-set-drive-count env name, satisfying the layer/facade guardrail (main's guardrail is stricter than when the core was authored). * fix(ecstore): use struct-init in erasure test helper to satisfy clippy field_reassign_with_default The cherry-picked fallible-erasure commit's `erasure_with_invalid_dimensions` test helper built `Erasure` via `default()` then reassigned fields, which trips `clippy::field_reassign_with_default` under `-D warnings` (only surfaced by `--all-targets`, which lints test code). #4977 fixed this in a later commit that was not part of the carved core. Use struct-init with `..Default::default()`, matching #4977's final form. * fix(rustfs): gate the test-only storage-class ENV facade re-export behind cfg(test) The ENV constants (INLINE_BLOCK_ENV/OPTIMIZE_ENV/RRS_ENV/STANDARD_ENV) re-exported through the startup storage facade are only consumed by a #[cfg(test)] test in startup_storage.rs, so in a non-test lib build the re-export is unused and trips -D unused-imports under clippy --all-targets. Gate it with #[cfg(test)], matching #4977's final form. --------- Co-authored-by: cxymds <cxymds@gmail.com>
This commit is contained in:
@@ -841,6 +841,12 @@ mod tests {
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncWrite, AsyncWriteExt};
|
||||
|
||||
fn erasure_with_zero_block_size() -> Erasure {
|
||||
let mut erasure = Erasure::default();
|
||||
erasure.data_shards = 1;
|
||||
erasure
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct DeferredCommitWriter {
|
||||
buffered: Vec<u8>,
|
||||
@@ -1500,7 +1506,7 @@ mod tests {
|
||||
HashAlgorithm::HighwayHash256S,
|
||||
))];
|
||||
|
||||
let erasure = Arc::new(Erasure::new(1, 0, 0));
|
||||
let erasure = Arc::new(erasure_with_zero_block_size());
|
||||
let reader = tokio::io::BufReader::new(Cursor::new(b"payload".to_vec()));
|
||||
let err = erasure
|
||||
.encode(reader, &mut writers, 1)
|
||||
@@ -1660,7 +1666,7 @@ mod tests {
|
||||
let writer = DeferredCommitWriter::new(committed);
|
||||
let mut writers = vec![Some(bitrot_writer(writer, 16))];
|
||||
|
||||
let erasure = Arc::new(Erasure::new(1, 0, 0));
|
||||
let erasure = Arc::new(erasure_with_zero_block_size());
|
||||
let reader = tokio::io::BufReader::new(Cursor::new(b"payload".to_vec()));
|
||||
let err = erasure
|
||||
.encode_batched(reader, &mut writers, 1)
|
||||
|
||||
@@ -25,6 +25,62 @@ use tokio::io::AsyncRead;
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
const MODERN_MAX_TOTAL_SHARDS: usize = <reed_solomon_erasure::galois_8::Field as reed_solomon_erasure::Field>::ORDER;
|
||||
|
||||
/// Errors returned when constructing an [`Erasure`] codec.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ErasureConstructionError {
|
||||
/// Erasure coding requires at least one data shard.
|
||||
#[error("data_shards must be greater than zero")]
|
||||
ZeroDataShards,
|
||||
|
||||
/// Erasure coding requires a non-zero logical block size.
|
||||
#[error("block_size must be greater than zero")]
|
||||
ZeroBlockSize,
|
||||
|
||||
/// The total shard count cannot be represented by `usize`.
|
||||
#[error("data_shards ({data_shards}) + parity_shards ({parity_shards}) overflows usize")]
|
||||
ShardCountOverflow { data_shards: usize, parity_shards: usize },
|
||||
|
||||
/// The modern GF(2^8) codec cannot represent this shard configuration.
|
||||
#[error("modern codec does not support {data_shards} data shards and {parity_shards} parity shards")]
|
||||
UnsupportedModernShardCount { data_shards: usize, parity_shards: usize },
|
||||
|
||||
/// The legacy SIMD codec cannot represent this shard configuration.
|
||||
#[error("legacy codec does not support {data_shards} data shards and {parity_shards} parity shards")]
|
||||
UnsupportedLegacyShardCount { data_shards: usize, parity_shards: usize },
|
||||
|
||||
/// The modern encoder rejected dimensions that passed the preflight checks.
|
||||
#[error("failed to construct modern Reed-Solomon encoder")]
|
||||
ModernEncoder {
|
||||
#[source]
|
||||
source: reed_solomon_erasure::Error,
|
||||
},
|
||||
|
||||
/// The legacy encoder wrapper failed to initialize.
|
||||
#[error("failed to construct legacy Reed-Solomon encoder")]
|
||||
LegacyEncoder {
|
||||
#[source]
|
||||
source: io::Error,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("{source}")]
|
||||
struct ErasureConstructionIoSource {
|
||||
#[source]
|
||||
source: ErasureConstructionError,
|
||||
}
|
||||
|
||||
impl ErasureConstructionError {
|
||||
pub(crate) fn into_io_error(self) -> io::Error {
|
||||
// `io::Error::source` forwards to the wrapped error's source rather
|
||||
// than exposing the wrapped error itself. This adapter keeps the
|
||||
// construction error as an observable link in the source chain.
|
||||
io::Error::other(ErasureConstructionIoSource { source: self })
|
||||
}
|
||||
}
|
||||
|
||||
/// Legacy calc_shard_size formula: (block_size.div_ceil(data_shards) + 1) & !1
|
||||
/// Matches main branch and filemeta::ErasureInfo for old-version files.
|
||||
pub fn calc_shard_size_legacy(block_size: usize, data_shards: usize) -> usize {
|
||||
@@ -233,12 +289,9 @@ impl Clone for ReedSolomonEncoder {
|
||||
}
|
||||
|
||||
impl ReedSolomonEncoder {
|
||||
/// Create a new Reed-Solomon encoder with specified data and parity shards.
|
||||
pub fn new(data_shards: usize, parity_shards: usize) -> io::Result<Self> {
|
||||
fn try_new_typed(data_shards: usize, parity_shards: usize) -> Result<Self, reed_solomon_erasure::Error> {
|
||||
let encoder = if parity_shards > 0 {
|
||||
ReedSolomon::new(data_shards, parity_shards)
|
||||
.map_err(|e| io::Error::other(format!("Failed to create Reed-Solomon encoder: {e:?}")))
|
||||
.map(Some)?
|
||||
Some(ReedSolomon::new(data_shards, parity_shards)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -250,6 +303,12 @@ impl ReedSolomonEncoder {
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new Reed-Solomon encoder with specified data and parity shards.
|
||||
pub fn new(data_shards: usize, parity_shards: usize) -> io::Result<Self> {
|
||||
Self::try_new_typed(data_shards, parity_shards)
|
||||
.map_err(|error| io::Error::other(format!("Failed to create Reed-Solomon encoder: {error:?}")))
|
||||
}
|
||||
|
||||
/// Encode data shards with parity.
|
||||
pub fn encode(&self, shards: SmallVec<[&mut [u8]; 16]>) -> io::Result<()> {
|
||||
let mut shards_vec: Vec<&mut [u8]> = shards.into_vec();
|
||||
@@ -464,28 +523,105 @@ impl Erasure {
|
||||
/// * `data_shards` - Number of data shards.
|
||||
/// * `parity_shards` - Number of parity shards.
|
||||
/// * `block_size` - Block size for each shard.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics when the dimensions are invalid or unsupported. RustFS production
|
||||
/// paths use [`Self::try_new`] instead.
|
||||
pub fn new(data_shards: usize, parity_shards: usize, block_size: usize) -> Self {
|
||||
Self::new_with_options(data_shards, parity_shards, block_size, false)
|
||||
match Self::try_new(data_shards, parity_shards, block_size) {
|
||||
Ok(erasure) => erasure,
|
||||
Err(error) => panic!("invalid erasure codec configuration: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new Erasure instance with legacy format support.
|
||||
///
|
||||
/// When `uses_legacy` is true, uses main-branch shard_size formula and reed-solomon-simd
|
||||
/// for decode/reconstruct (for reading and healing old-version files).
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics when the dimensions are invalid or unsupported. RustFS production
|
||||
/// paths use [`Self::try_new_with_options`] instead.
|
||||
pub fn new_with_options(data_shards: usize, parity_shards: usize, block_size: usize, uses_legacy: bool) -> Self {
|
||||
match Self::try_new_with_options(data_shards, parity_shards, block_size, uses_legacy) {
|
||||
Ok(erasure) => erasure,
|
||||
Err(error) => panic!("invalid erasure codec configuration: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to create a modern Erasure codec.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`ErasureConstructionError`] when dimensions are zero,
|
||||
/// overflow the shard count, exceed the modern codec's supported range, or
|
||||
/// the underlying encoder rejects the configuration.
|
||||
pub fn try_new(data_shards: usize, parity_shards: usize, block_size: usize) -> Result<Self, ErasureConstructionError> {
|
||||
Self::try_new_with_options(data_shards, parity_shards, block_size, false)
|
||||
}
|
||||
|
||||
/// Tries to create an Erasure codec using the selected format backend.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`ErasureConstructionError`] when dimensions are zero,
|
||||
/// overflow the shard count, are unsupported by the selected codec, or the
|
||||
/// selected encoder fails to initialize.
|
||||
pub fn try_new_with_options(
|
||||
data_shards: usize,
|
||||
parity_shards: usize,
|
||||
block_size: usize,
|
||||
uses_legacy: bool,
|
||||
) -> Result<Self, ErasureConstructionError> {
|
||||
if data_shards == 0 {
|
||||
return Err(ErasureConstructionError::ZeroDataShards);
|
||||
}
|
||||
if block_size == 0 {
|
||||
return Err(ErasureConstructionError::ZeroBlockSize);
|
||||
}
|
||||
|
||||
let total_shards = data_shards
|
||||
.checked_add(parity_shards)
|
||||
.ok_or(ErasureConstructionError::ShardCountOverflow {
|
||||
data_shards,
|
||||
parity_shards,
|
||||
})?;
|
||||
|
||||
if parity_shards > 0 {
|
||||
if uses_legacy
|
||||
&& (total_shards > reed_solomon_simd::engine::GF_ORDER
|
||||
|| !reed_solomon_simd::ReedSolomonEncoder::supports(data_shards, parity_shards))
|
||||
{
|
||||
return Err(ErasureConstructionError::UnsupportedLegacyShardCount {
|
||||
data_shards,
|
||||
parity_shards,
|
||||
});
|
||||
}
|
||||
if !uses_legacy && total_shards > MODERN_MAX_TOTAL_SHARDS {
|
||||
return Err(ErasureConstructionError::UnsupportedModernShardCount {
|
||||
data_shards,
|
||||
parity_shards,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let encoder = if !uses_legacy && parity_shards > 0 {
|
||||
Some(ReedSolomonEncoder::new(data_shards, parity_shards).expect("operation should succeed"))
|
||||
Some(
|
||||
ReedSolomonEncoder::try_new_typed(data_shards, parity_shards)
|
||||
.map_err(|source| ErasureConstructionError::ModernEncoder { source })?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let legacy_encoder = if uses_legacy && parity_shards > 0 {
|
||||
Some(LegacyReedSolomonEncoder::new(data_shards, parity_shards).expect("operation should succeed"))
|
||||
Some(
|
||||
LegacyReedSolomonEncoder::new(data_shards, parity_shards)
|
||||
.map_err(|source| ErasureConstructionError::LegacyEncoder { source })?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Erasure {
|
||||
Ok(Erasure {
|
||||
data_shards,
|
||||
parity_shards,
|
||||
block_size,
|
||||
@@ -493,7 +629,7 @@ impl Erasure {
|
||||
legacy_encoder,
|
||||
uses_legacy,
|
||||
_id: Uuid::nil(), // Unused in hot paths; avoid CSPRNG syscall
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Encode data into data and parity shards.
|
||||
@@ -946,6 +1082,15 @@ mod tests {
|
||||
use std::task::{Context, Poll};
|
||||
use tokio::io::ReadBuf;
|
||||
|
||||
fn erasure_with_invalid_dimensions(data_shards: usize, parity_shards: usize, block_size: usize) -> Erasure {
|
||||
Erasure {
|
||||
data_shards,
|
||||
parity_shards,
|
||||
block_size,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn optional_shards(shards: &[Bytes]) -> Vec<Option<Vec<u8>>> {
|
||||
shards.iter().map(|shard| Some(shard.to_vec())).collect()
|
||||
}
|
||||
@@ -1010,6 +1155,156 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_new_rejects_zero_and_overflowing_dimensions_with_typed_errors() {
|
||||
let Err(zero_data) = Erasure::try_new(0, 2, 64) else {
|
||||
panic!("zero data shards must be rejected");
|
||||
};
|
||||
assert!(matches!(zero_data, ErasureConstructionError::ZeroDataShards));
|
||||
|
||||
let Err(zero_block) = Erasure::try_new(2, 2, 0) else {
|
||||
panic!("zero block size must be rejected");
|
||||
};
|
||||
assert!(matches!(zero_block, ErasureConstructionError::ZeroBlockSize));
|
||||
|
||||
let Err(overflow) = Erasure::try_new(usize::MAX, 1, 64) else {
|
||||
panic!("overflowing shard counts must be rejected");
|
||||
};
|
||||
assert!(matches!(
|
||||
overflow,
|
||||
ErasureConstructionError::ShardCountOverflow {
|
||||
data_shards: usize::MAX,
|
||||
parity_shards: 1,
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_new_rejects_backend_specific_unsupported_dimensions() {
|
||||
let Err(modern) = Erasure::try_new(256, 1, 64) else {
|
||||
panic!("modern dimensions beyond GF(2^8) must be rejected");
|
||||
};
|
||||
assert!(matches!(
|
||||
modern,
|
||||
ErasureConstructionError::UnsupportedModernShardCount {
|
||||
data_shards: 256,
|
||||
parity_shards: 1,
|
||||
}
|
||||
));
|
||||
|
||||
let Err(legacy) = Erasure::try_new_with_options(60_000, 5_000, 64, true) else {
|
||||
panic!("unsupported legacy SIMD dimensions must be rejected");
|
||||
};
|
||||
assert!(matches!(
|
||||
legacy,
|
||||
ErasureConstructionError::UnsupportedLegacyShardCount {
|
||||
data_shards: 60_000,
|
||||
parity_shards: 5_000,
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_new_accepts_exact_backend_shard_limits() {
|
||||
let modern_data_shards = MODERN_MAX_TOTAL_SHARDS - 1;
|
||||
let modern =
|
||||
Erasure::try_new(modern_data_shards, 1, 64).expect("the modern codec's exact field-size boundary must remain valid");
|
||||
assert_eq!(modern.total_shard_count(), MODERN_MAX_TOTAL_SHARDS);
|
||||
assert!(modern.encoder.is_some());
|
||||
assert!(modern.legacy_encoder.is_none());
|
||||
|
||||
let legacy_data_shards = reed_solomon_simd::engine::GF_ORDER / 2;
|
||||
let legacy_parity_shards = reed_solomon_simd::engine::GF_ORDER - legacy_data_shards;
|
||||
let legacy = Erasure::try_new_with_options(legacy_data_shards, legacy_parity_shards, 64, true)
|
||||
.expect("the legacy codec's exact field-size boundary must remain valid");
|
||||
assert_eq!(legacy.total_shard_count(), reed_solomon_simd::engine::GF_ORDER);
|
||||
assert!(legacy.encoder.is_none());
|
||||
assert!(legacy.legacy_encoder.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_new_accepts_zero_parity_without_backend_limits() {
|
||||
let no_parity = Erasure::try_new(2, 0, 64).expect("zero parity is a valid generic codec configuration");
|
||||
assert_eq!(no_parity.total_shard_count(), 2);
|
||||
assert!(no_parity.encoder.is_none());
|
||||
assert!(no_parity.legacy_encoder.is_none());
|
||||
|
||||
let large_modern = Erasure::try_new(257, 0, 64).expect("zero parity must not inherit the modern backend shard limit");
|
||||
assert_eq!(large_modern.total_shard_count(), 257);
|
||||
assert!(large_modern.encoder.is_none());
|
||||
assert!(large_modern.legacy_encoder.is_none());
|
||||
|
||||
let legacy_data_shards = reed_solomon_simd::engine::GF_ORDER + 1;
|
||||
let large_legacy = Erasure::try_new_with_options(legacy_data_shards, 0, 64, true)
|
||||
.expect("zero parity must not inherit the legacy backend shard limit");
|
||||
assert_eq!(large_legacy.total_shard_count(), legacy_data_shards);
|
||||
assert!(large_legacy.encoder.is_none());
|
||||
assert!(large_legacy.legacy_encoder.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_new_accepts_generic_layouts_above_storage_drive_limits() {
|
||||
let erasure = Erasure::try_new(2, 3, 64).expect("generic 2+3 erasure coding must remain supported");
|
||||
assert_eq!(erasure.total_shard_count(), 5);
|
||||
assert!(erasure.encoder.is_some());
|
||||
assert!(erasure.legacy_encoder.is_none());
|
||||
|
||||
let modern = Erasure::try_new(9, 8, 64).expect("the codec must not impose the storage layer's drive limit");
|
||||
assert_eq!(modern.total_shard_count(), 17);
|
||||
assert!(modern.encoder.is_some());
|
||||
assert!(modern.legacy_encoder.is_none());
|
||||
|
||||
let legacy = Erasure::try_new_with_options(9, 8, 64, true)
|
||||
.expect("the legacy codec must not impose the storage layer's drive limit");
|
||||
assert_eq!(legacy.total_shard_count(), 17);
|
||||
assert!(legacy.encoder.is_none());
|
||||
assert!(legacy.legacy_encoder.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_new_with_options_initializes_only_the_selected_backend() {
|
||||
let modern = Erasure::try_new_with_options(4, 2, 64, false).expect("modern codec should construct");
|
||||
assert!(modern.encoder.is_some());
|
||||
assert!(modern.legacy_encoder.is_none());
|
||||
|
||||
let legacy = Erasure::try_new_with_options(4, 2, 64, true).expect("legacy codec should construct");
|
||||
assert!(legacy.encoder.is_none());
|
||||
assert!(legacy.legacy_encoder.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construction_errors_preserve_encoder_sources() {
|
||||
let modern = ErasureConstructionError::ModernEncoder {
|
||||
source: reed_solomon_erasure::Error::TooManyShards,
|
||||
};
|
||||
assert!(
|
||||
std::error::Error::source(&modern)
|
||||
.and_then(|source| source.downcast_ref::<reed_solomon_erasure::Error>())
|
||||
.is_some()
|
||||
);
|
||||
|
||||
let legacy = ErasureConstructionError::LegacyEncoder {
|
||||
source: io::Error::other("legacy encoder failure"),
|
||||
};
|
||||
assert!(
|
||||
std::error::Error::source(&legacy)
|
||||
.and_then(|source| source.downcast_ref::<io::Error>())
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "invalid erasure codec configuration")]
|
||||
fn new_panics_on_invalid_dimensions() {
|
||||
let _ = Erasure::new(0, 1, 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "invalid erasure codec configuration")]
|
||||
fn new_with_options_panics_on_invalid_dimensions() {
|
||||
let _ = Erasure::new_with_options(1, 1, 0, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has_valid_dimensions_rejects_zero_block_size_or_data_shards() {
|
||||
// Well-formed erasure metadata is accepted.
|
||||
@@ -1018,11 +1313,9 @@ mod tests {
|
||||
|
||||
// Corrupted on-disk metadata with a zero block_size or zero data_shards
|
||||
// must be rejected before it reaches the shard/offset divisions.
|
||||
// (parity_shards is kept 0 for the zero-data_shards cases so the
|
||||
// Reed-Solomon encoder is not constructed with an invalid shard count.)
|
||||
assert!(!Erasure::new(4, 2, 0).has_valid_dimensions());
|
||||
assert!(!Erasure::new(0, 0, 64).has_valid_dimensions());
|
||||
assert!(!Erasure::new(0, 0, 0).has_valid_dimensions());
|
||||
assert!(!erasure_with_invalid_dimensions(4, 2, 0).has_valid_dimensions());
|
||||
assert!(!erasure_with_invalid_dimensions(0, 0, 64).has_valid_dimensions());
|
||||
assert!(!erasure_with_invalid_dimensions(0, 0, 0).has_valid_dimensions());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1762,7 +2055,7 @@ mod tests {
|
||||
use std::io::Cursor;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
let erasure = Arc::new(Erasure::new(1, 0, 0));
|
||||
let erasure = Arc::new(erasure_with_invalid_dimensions(1, 0, 0));
|
||||
let mut reader = Cursor::new(b"payload".to_vec());
|
||||
let observed = Arc::new(Mutex::new(None));
|
||||
let observed_clone = observed.clone();
|
||||
|
||||
@@ -20,4 +20,4 @@ pub mod erasure;
|
||||
pub mod heal;
|
||||
pub use bitrot::*;
|
||||
|
||||
pub use erasure::{Erasure, ReedSolomonEncoder, calc_shard_size, calc_shard_size_legacy};
|
||||
pub use erasure::{Erasure, ErasureConstructionError, ReedSolomonEncoder, calc_shard_size, calc_shard_size_legacy};
|
||||
|
||||
Reference in New Issue
Block a user