mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-16 18:08:21 +00:00
eb41f45175
Removing both blankets exposes 23 items, of which only four are deleted. The ratio is the point: close to the core data path the blankets were hiding test assertions and migration seams, not dead code. A cfg-split function is the reason two symbols in the internode transport look dead when neither is. build_internode_data_transport_from_env has two bodies, one under #[cfg(test)] that calls build_internode_data_transport directly and one under #[cfg(not(test))] that goes through the INTERNODE_DATA_TRANSPORT static so tests do not share process-global transport state. Each half's helper is live in exactly one build, and because cargo check --tests compiles both the lib target and the test harness, both symbols appear in one warning list. Deleting either one breaks the other lane. Both are kept with allows naming their half. Three deletion candidates were withdrawn after a per-name grep: ParallelReader::new, ErasureDecodeReader::new and SyncErasureDecodeReader::new all have test callers. The last two are exactly the shape of the dead wrapper deleted in #6084 — a thin forward to a new_with_metrics_path sibling — except that sibling is live in production (set_disk/read.rs) and the wrappers are used by tests. Deleted: - RemotePeerS3Client::get_addr and RemoteLocker::from_url, neither with a consumer in any lane. - RemotePeerS3Client's node field, which new writes after using it to derive addr and nothing ever reads. Its only other writer was a test helper that built a whole Node solely to fill the field; that block goes too. - ParallelReader::can_decode, superseded by an inlined copy. The copy's comment named the method it replaced, so deleting the method alone would have left a dangling reference; the comment now describes the check instead of pointing at a method that no longer exists. Kept with allows: the erasure items are decode/encode invariants asserted by their own files' tests (shard_read_launch_order, decode_with_read_costs, emit_data_shards, queued_block_bytes, the engine trait facets, the ParallelReader and decode-reader constructors, encode_stream_callback_async). On the cluster side, peer_replay_state, heal_bucket_local and clone_drives are test-only, InternodeDataTransportCapabilities and tcp_http are constructed only by transport test doubles, and the InternodeDataTransport trait's name/capabilities pair is an unused capability-negotiation facet kept for the transport split (backlog#1350) — six impls provide them and no caller negotiates on them yet. Verification, four lanes warning-free: default, --tests, --features rio-v2 --tests, --features test-util --tests. cargo nextest run -p rustfs-ecstore 4041 passed; clippy --lib --tests -D warnings clean; make pre-commit exit 0. Ref rustfs/backlog#1823 (step 2).
795 lines
30 KiB
Rust
795 lines
30 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
use crate::erasure::codec::workspace::RustfsCodecDecodeWorkspace;
|
|
use crate::erasure::coding::Erasure;
|
|
use reed_solomon_erasure::galois_8::ReedSolomon;
|
|
use std::io;
|
|
use std::sync::{Arc, OnceLock};
|
|
|
|
pub(crate) const GET_CODEC_STREAMING_ENGINE_LEGACY: &str = "legacy";
|
|
pub(crate) const GET_CODEC_STREAMING_ENGINE_RUSTFS: &str = "rustfs";
|
|
pub(crate) const GET_RECONSTRUCT_OUTCOME_LEGACY_CALLED: &str = "legacy_called";
|
|
pub(crate) const GET_RECONSTRUCT_OUTCOME_RUSTFS_CALLED: &str = "rustfs_called";
|
|
pub(crate) const GET_RECONSTRUCT_OUTCOME_SKIP_DATA_COMPLETE: &str = "skip_data_complete";
|
|
pub(crate) const GET_RECONSTRUCT_OUTCOME_SKIP_EMPTY_PAYLOAD: &str = "skip_empty_payload";
|
|
|
|
pub(crate) trait DecodeWorkspace: Send + Sync + 'static {
|
|
#[allow(dead_code, reason = "workspace width asserted by decode_reader tests (backlog#1823)")]
|
|
fn shard_len(&self) -> usize;
|
|
}
|
|
|
|
pub(crate) trait ErasureDecodeEngine: Send + Sync + 'static {
|
|
type Workspace: DecodeWorkspace;
|
|
|
|
fn data_shards(&self) -> usize;
|
|
#[allow(dead_code, reason = "engine trait facet asserted by decode_reader tests (backlog#1823)")]
|
|
fn parity_shards(&self) -> usize;
|
|
fn block_size(&self) -> usize;
|
|
fn engine_name(&self) -> &'static str;
|
|
|
|
#[allow(dead_code, reason = "engine trait facet asserted by decode_reader tests (backlog#1823)")]
|
|
fn supports_progressive_decode(&self) -> bool;
|
|
#[allow(dead_code, reason = "engine trait facet asserted by decode_reader tests (backlog#1823)")]
|
|
fn supports_aligned_shards(&self) -> bool;
|
|
|
|
fn prepare_workspace(&self, shard_len: usize) -> io::Result<Self::Workspace>;
|
|
|
|
fn reconstruct_into(&self, shards: &mut [Option<Vec<u8>>], workspace: &mut Self::Workspace) -> io::Result<&'static str>;
|
|
}
|
|
|
|
fn data_shards_complete(shards: &[Option<Vec<u8>>], data_shards: usize) -> bool {
|
|
shards.len() >= data_shards && shards.iter().take(data_shards).all(Option::is_some)
|
|
}
|
|
|
|
fn recover_empty_payload_data_shards(
|
|
shards: &mut [Option<Vec<u8>>],
|
|
data_shards: usize,
|
|
parity_shards: usize,
|
|
) -> io::Result<bool> {
|
|
let expected_shards = data_shards + parity_shards;
|
|
if shards.len() != expected_shards {
|
|
return Err(io::Error::other(format!(
|
|
"invalid shard count: got {}, expected {}",
|
|
shards.len(),
|
|
expected_shards
|
|
)));
|
|
}
|
|
|
|
let mut present_shards = 0usize;
|
|
for shard in shards.iter().filter_map(Option::as_ref) {
|
|
present_shards += 1;
|
|
if !shard.is_empty() {
|
|
return Ok(false);
|
|
}
|
|
}
|
|
if present_shards == 0 || present_shards < data_shards {
|
|
return Ok(false);
|
|
}
|
|
|
|
for shard in shards.iter_mut().take(data_shards) {
|
|
if shard.is_none() {
|
|
*shard = Some(Vec::new());
|
|
}
|
|
}
|
|
|
|
Ok(true)
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub(crate) struct LegacyDecodeWorkspace {
|
|
shard_len: usize,
|
|
}
|
|
|
|
impl LegacyDecodeWorkspace {
|
|
fn new(shard_len: usize) -> Self {
|
|
Self { shard_len }
|
|
}
|
|
}
|
|
|
|
impl DecodeWorkspace for RustfsCodecDecodeWorkspace {
|
|
fn shard_len(&self) -> usize {
|
|
RustfsCodecDecodeWorkspace::shard_len(self)
|
|
}
|
|
}
|
|
|
|
impl DecodeWorkspace for LegacyDecodeWorkspace {
|
|
fn shard_len(&self) -> usize {
|
|
self.shard_len
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) struct LegacyEcDecodeEngine {
|
|
erasure: Erasure,
|
|
}
|
|
|
|
impl LegacyEcDecodeEngine {
|
|
pub(crate) fn new(erasure: Erasure) -> Self {
|
|
Self { erasure }
|
|
}
|
|
}
|
|
|
|
impl ErasureDecodeEngine for LegacyEcDecodeEngine {
|
|
type Workspace = LegacyDecodeWorkspace;
|
|
|
|
fn data_shards(&self) -> usize {
|
|
self.erasure.data_shards
|
|
}
|
|
|
|
fn parity_shards(&self) -> usize {
|
|
self.erasure.parity_shards
|
|
}
|
|
|
|
fn block_size(&self) -> usize {
|
|
self.erasure.block_size
|
|
}
|
|
|
|
fn engine_name(&self) -> &'static str {
|
|
GET_CODEC_STREAMING_ENGINE_LEGACY
|
|
}
|
|
|
|
fn supports_progressive_decode(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn supports_aligned_shards(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn prepare_workspace(&self, shard_len: usize) -> io::Result<Self::Workspace> {
|
|
Ok(LegacyDecodeWorkspace::new(shard_len))
|
|
}
|
|
|
|
fn reconstruct_into(&self, shards: &mut [Option<Vec<u8>>], _workspace: &mut Self::Workspace) -> io::Result<&'static str> {
|
|
if data_shards_complete(shards, self.erasure.data_shards) {
|
|
return Ok(GET_RECONSTRUCT_OUTCOME_SKIP_DATA_COMPLETE);
|
|
}
|
|
|
|
self.erasure.decode_data_with_reconstruction_verification(shards)?;
|
|
Ok(GET_RECONSTRUCT_OUTCOME_LEGACY_CALLED)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) struct RustfsCodecDecodeEngine {
|
|
data_shards: usize,
|
|
parity_shards: usize,
|
|
block_size: usize,
|
|
codec: OnceLock<Arc<ReedSolomon>>,
|
|
}
|
|
|
|
impl RustfsCodecDecodeEngine {
|
|
pub(crate) fn new(erasure: &Erasure) -> io::Result<Self> {
|
|
Ok(Self {
|
|
data_shards: erasure.data_shards,
|
|
parity_shards: erasure.parity_shards,
|
|
block_size: erasure.block_size,
|
|
codec: OnceLock::new(),
|
|
})
|
|
}
|
|
|
|
fn codec(&self) -> io::Result<Option<Arc<ReedSolomon>>> {
|
|
if self.parity_shards == 0 {
|
|
return Ok(None);
|
|
}
|
|
|
|
if let Some(codec) = self.codec.get() {
|
|
return Ok(Some(Arc::clone(codec)));
|
|
}
|
|
|
|
let codec = Arc::new(
|
|
ReedSolomon::new(self.data_shards, self.parity_shards)
|
|
.map_err(|err| io::Error::other(format!("Failed to create RustFS codec decode engine: {err:?}")))?,
|
|
);
|
|
if self.codec.set(Arc::clone(&codec)).is_err() {
|
|
return Ok(self.codec.get().map(Arc::clone).or(Some(codec)));
|
|
}
|
|
Ok(Some(codec))
|
|
}
|
|
|
|
fn needs_source_parity_verification(&self, shards: &[Option<Vec<u8>>]) -> bool {
|
|
let missing_data_source = shards.iter().take(self.data_shards).any(|shard| shard.is_none());
|
|
let available_shards = shards.iter().filter(|shard| shard.is_some()).count();
|
|
missing_data_source && available_shards > self.data_shards
|
|
}
|
|
|
|
fn verify_source_parity(&self, codec: &ReedSolomon, shards: &[Option<Vec<u8>>], needs_verification: bool) -> io::Result<()> {
|
|
if !needs_verification {
|
|
return Ok(());
|
|
}
|
|
|
|
// All slots must be present here: the verification path reconstructs
|
|
// missing parity alongside data (see `reconstruct_into`), so a `None`
|
|
// slot is an internal invariant violation, not a degraded-read state.
|
|
let mut shard_refs = Vec::with_capacity(self.data_shards + self.parity_shards);
|
|
for (index, shard) in shards.iter().enumerate() {
|
|
let shard = shard.as_ref().ok_or_else(|| {
|
|
io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
format!("missing shard {index} after RustFS codec reconstruction"),
|
|
)
|
|
})?;
|
|
shard_refs.push(shard.as_slice());
|
|
}
|
|
|
|
let valid = codec
|
|
.verify(&shard_refs)
|
|
.map_err(|err| io::Error::other(format!("RustFS codec verify failed: {err:?}")))?;
|
|
if !valid {
|
|
return Err(io::Error::new(io::ErrorKind::InvalidData, "inconsistent read source shards"));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn codec_is_initialized(&self) -> bool {
|
|
self.codec.get().is_some()
|
|
}
|
|
}
|
|
|
|
impl ErasureDecodeEngine for RustfsCodecDecodeEngine {
|
|
type Workspace = RustfsCodecDecodeWorkspace;
|
|
|
|
fn data_shards(&self) -> usize {
|
|
self.data_shards
|
|
}
|
|
|
|
fn parity_shards(&self) -> usize {
|
|
self.parity_shards
|
|
}
|
|
|
|
fn block_size(&self) -> usize {
|
|
self.block_size
|
|
}
|
|
|
|
fn engine_name(&self) -> &'static str {
|
|
GET_CODEC_STREAMING_ENGINE_RUSTFS
|
|
}
|
|
|
|
fn supports_progressive_decode(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn supports_aligned_shards(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn prepare_workspace(&self, shard_len: usize) -> io::Result<Self::Workspace> {
|
|
Ok(RustfsCodecDecodeWorkspace::new(shard_len))
|
|
}
|
|
|
|
fn reconstruct_into(&self, shards: &mut [Option<Vec<u8>>], _workspace: &mut Self::Workspace) -> io::Result<&'static str> {
|
|
if data_shards_complete(shards, self.data_shards) {
|
|
return Ok(GET_RECONSTRUCT_OUTCOME_SKIP_DATA_COMPLETE);
|
|
}
|
|
if recover_empty_payload_data_shards(shards, self.data_shards, self.parity_shards)? {
|
|
return Ok(GET_RECONSTRUCT_OUTCOME_SKIP_EMPTY_PAYLOAD);
|
|
}
|
|
|
|
if let Some(codec) = self.codec()? {
|
|
let needs_source_parity_verification = self.needs_source_parity_verification(shards);
|
|
if needs_source_parity_verification {
|
|
// Rebuild missing parity together with missing data so the
|
|
// shard set is complete for `verify`. Rebuilt parity is
|
|
// consistent with the reconstructed data by construction, so
|
|
// the verification below is equivalent to checking only the
|
|
// originally-present source parity — the same semantics as the
|
|
// legacy `decode_data_with_reconstruction_verification` path.
|
|
// Rebuilding only data here would leave missing parity slots
|
|
// as `None` and misreport a recoverable degraded read (for
|
|
// example one missing data shard plus one missing parity
|
|
// shard) as an inconsistent-source failure.
|
|
codec
|
|
.reconstruct_opt(shards)
|
|
.map_err(|err| io::Error::other(format!("RustFS codec reconstruct failed: {err:?}")))?;
|
|
} else {
|
|
codec
|
|
.reconstruct_data_opt(shards)
|
|
.map_err(|err| io::Error::other(format!("RustFS codec reconstruct failed: {err:?}")))?;
|
|
}
|
|
self.verify_source_parity(&codec, shards, needs_source_parity_verification)?;
|
|
}
|
|
|
|
Ok(GET_RECONSTRUCT_OUTCOME_RUSTFS_CALLED)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) enum CodecStreamingDecodeEngine {
|
|
Legacy(Box<LegacyEcDecodeEngine>),
|
|
Rustfs(Box<RustfsCodecDecodeEngine>),
|
|
}
|
|
|
|
impl CodecStreamingDecodeEngine {
|
|
pub(crate) fn legacy(erasure: Erasure) -> Self {
|
|
Self::Legacy(Box::new(LegacyEcDecodeEngine::new(erasure)))
|
|
}
|
|
|
|
pub(crate) fn rustfs(erasure: &Erasure) -> io::Result<Self> {
|
|
RustfsCodecDecodeEngine::new(erasure).map(Box::new).map(Self::Rustfs)
|
|
}
|
|
}
|
|
|
|
pub(crate) enum CodecStreamingDecodeWorkspace {
|
|
Legacy(LegacyDecodeWorkspace),
|
|
Rustfs(RustfsCodecDecodeWorkspace),
|
|
}
|
|
|
|
impl DecodeWorkspace for CodecStreamingDecodeWorkspace {
|
|
fn shard_len(&self) -> usize {
|
|
match self {
|
|
Self::Legacy(workspace) => workspace.shard_len(),
|
|
Self::Rustfs(workspace) => workspace.shard_len(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ErasureDecodeEngine for CodecStreamingDecodeEngine {
|
|
type Workspace = CodecStreamingDecodeWorkspace;
|
|
|
|
fn data_shards(&self) -> usize {
|
|
match self {
|
|
Self::Legacy(engine) => engine.data_shards(),
|
|
Self::Rustfs(engine) => engine.data_shards(),
|
|
}
|
|
}
|
|
|
|
fn parity_shards(&self) -> usize {
|
|
match self {
|
|
Self::Legacy(engine) => engine.parity_shards(),
|
|
Self::Rustfs(engine) => engine.parity_shards(),
|
|
}
|
|
}
|
|
|
|
fn block_size(&self) -> usize {
|
|
match self {
|
|
Self::Legacy(engine) => engine.block_size(),
|
|
Self::Rustfs(engine) => engine.block_size(),
|
|
}
|
|
}
|
|
|
|
fn engine_name(&self) -> &'static str {
|
|
match self {
|
|
Self::Legacy(engine) => engine.engine_name(),
|
|
Self::Rustfs(engine) => engine.engine_name(),
|
|
}
|
|
}
|
|
|
|
fn supports_progressive_decode(&self) -> bool {
|
|
match self {
|
|
Self::Legacy(engine) => engine.supports_progressive_decode(),
|
|
Self::Rustfs(engine) => engine.supports_progressive_decode(),
|
|
}
|
|
}
|
|
|
|
fn supports_aligned_shards(&self) -> bool {
|
|
match self {
|
|
Self::Legacy(engine) => engine.supports_aligned_shards(),
|
|
Self::Rustfs(engine) => engine.supports_aligned_shards(),
|
|
}
|
|
}
|
|
|
|
fn prepare_workspace(&self, shard_len: usize) -> io::Result<Self::Workspace> {
|
|
match self {
|
|
Self::Legacy(engine) => engine.prepare_workspace(shard_len).map(CodecStreamingDecodeWorkspace::Legacy),
|
|
Self::Rustfs(engine) => engine.prepare_workspace(shard_len).map(CodecStreamingDecodeWorkspace::Rustfs),
|
|
}
|
|
}
|
|
|
|
fn reconstruct_into(&self, shards: &mut [Option<Vec<u8>>], workspace: &mut Self::Workspace) -> io::Result<&'static str> {
|
|
match (self, workspace) {
|
|
(Self::Legacy(engine), CodecStreamingDecodeWorkspace::Legacy(workspace)) => {
|
|
engine.reconstruct_into(shards, workspace)
|
|
}
|
|
(Self::Rustfs(engine), CodecStreamingDecodeWorkspace::Rustfs(workspace)) => {
|
|
engine.reconstruct_into(shards, workspace)
|
|
}
|
|
_ => Err(io::Error::other("codec streaming decode engine/workspace mismatch")),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn encoded_shards(erasure: &Erasure, data: &[u8]) -> Vec<Option<Vec<u8>>> {
|
|
erasure
|
|
.encode_data(data)
|
|
.expect("test stripe should encode")
|
|
.into_iter()
|
|
.map(|shard| Some(shard.to_vec()))
|
|
.collect()
|
|
}
|
|
|
|
fn reconstruct_with<E>(engine: &E, shards: &mut [Option<Vec<u8>>]) -> io::Result<()>
|
|
where
|
|
E: ErasureDecodeEngine,
|
|
{
|
|
let mut workspace = engine.prepare_workspace(4)?;
|
|
engine.reconstruct_into(shards, &mut workspace).map(|_| ())
|
|
}
|
|
|
|
#[test]
|
|
fn data_shards_complete_ignores_parity_slots() {
|
|
let shards = vec![Some(vec![1]), Some(vec![2]), None];
|
|
|
|
assert!(data_shards_complete(&shards, 2));
|
|
}
|
|
|
|
#[test]
|
|
fn data_shards_complete_requires_all_data_slots() {
|
|
let missing_data = vec![Some(vec![1]), None, Some(vec![3])];
|
|
let short = vec![Some(vec![1])];
|
|
|
|
assert!(!data_shards_complete(&missing_data, 2));
|
|
assert!(!data_shards_complete(&short, 2));
|
|
}
|
|
|
|
#[test]
|
|
fn legacy_decode_engine_reports_erasure_shape() {
|
|
let engine = LegacyEcDecodeEngine::new(Erasure::new(4, 2, 1 << 20));
|
|
|
|
assert_eq!(engine.data_shards(), 4);
|
|
assert_eq!(engine.parity_shards(), 2);
|
|
assert_eq!(engine.block_size(), 1 << 20);
|
|
assert!(!engine.supports_progressive_decode());
|
|
assert!(!engine.supports_aligned_shards());
|
|
}
|
|
|
|
#[test]
|
|
fn legacy_decode_engine_reconstructs_missing_data_shard() {
|
|
let erasure = Erasure::new(4, 2, 16);
|
|
let encoded = erasure
|
|
.encode_data(b"codec bridge keeps current reconstruction behavior")
|
|
.expect("encode should succeed");
|
|
let mut shards = encoded.into_iter().map(|shard| Some(shard.to_vec())).collect::<Vec<_>>();
|
|
shards[1] = None;
|
|
|
|
let engine = LegacyEcDecodeEngine::new(erasure);
|
|
let mut workspace = engine.prepare_workspace(4).expect("workspace should be prepared");
|
|
engine
|
|
.reconstruct_into(&mut shards, &mut workspace)
|
|
.expect("legacy engine should reconstruct through current erasure path");
|
|
|
|
assert_eq!(workspace.shard_len(), 4);
|
|
assert!(shards.iter().take(engine.data_shards()).all(Option::is_some));
|
|
}
|
|
|
|
#[test]
|
|
fn rustfs_codec_decode_engine_reports_erasure_shape() {
|
|
let erasure = Erasure::new(4, 2, 1 << 20);
|
|
let engine = RustfsCodecDecodeEngine::new(&erasure).expect("engine should be created");
|
|
|
|
assert_eq!(engine.data_shards(), 4);
|
|
assert_eq!(engine.parity_shards(), 2);
|
|
assert_eq!(engine.block_size(), 1 << 20);
|
|
assert!(!engine.supports_progressive_decode());
|
|
assert!(!engine.supports_aligned_shards());
|
|
}
|
|
|
|
#[test]
|
|
fn rustfs_codec_decode_engine_keeps_complete_data_shards() {
|
|
let erasure = Erasure::new(4, 2, 16);
|
|
let mut shards = encoded_shards(&erasure, b"all data shards are present");
|
|
let before = shards.clone();
|
|
|
|
let engine = RustfsCodecDecodeEngine::new(&erasure).expect("engine should be created");
|
|
assert!(!engine.codec_is_initialized());
|
|
reconstruct_with(&engine, &mut shards).expect("complete data shards should not reconstruct");
|
|
|
|
assert!(!engine.codec_is_initialized());
|
|
assert_eq!(shards, before);
|
|
}
|
|
|
|
#[test]
|
|
fn rustfs_codec_decode_engine_reconstructs_missing_data_like_legacy() {
|
|
let erasure = Erasure::new(4, 2, 16);
|
|
let mut legacy_shards = encoded_shards(&erasure, b"missing data shard must match legacy output");
|
|
let mut rustfs_shards = legacy_shards.clone();
|
|
legacy_shards[1] = None;
|
|
rustfs_shards[1] = None;
|
|
|
|
let legacy = LegacyEcDecodeEngine::new(erasure.clone());
|
|
let rustfs = RustfsCodecDecodeEngine::new(&erasure).expect("engine should be created");
|
|
|
|
reconstruct_with(&legacy, &mut legacy_shards).expect("legacy should reconstruct");
|
|
reconstruct_with(&rustfs, &mut rustfs_shards).expect("rustfs codec should reconstruct");
|
|
|
|
assert_eq!(rustfs_shards, legacy_shards);
|
|
}
|
|
|
|
#[test]
|
|
fn rustfs_codec_decode_engine_leaves_missing_parity_unreconstructed() {
|
|
let erasure = Erasure::new(4, 2, 16);
|
|
let mut shards = encoded_shards(&erasure, b"parity-only missing should not touch output data");
|
|
let before_data = shards.iter().take(erasure.data_shards).cloned().collect::<Vec<_>>();
|
|
shards[erasure.data_shards] = None;
|
|
|
|
let engine = RustfsCodecDecodeEngine::new(&erasure).expect("engine should be created");
|
|
reconstruct_with(&engine, &mut shards).expect("missing parity should not fail");
|
|
|
|
assert_eq!(shards.iter().take(erasure.data_shards).cloned().collect::<Vec<_>>(), before_data);
|
|
assert!(shards[erasure.data_shards].is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn rustfs_codec_decode_engine_errors_on_insufficient_shards_like_legacy() {
|
|
let erasure = Erasure::new(4, 2, 16);
|
|
let mut legacy_shards = encoded_shards(&erasure, b"insufficient shards must fail");
|
|
let mut rustfs_shards = legacy_shards.clone();
|
|
for index in [0, 1, 2] {
|
|
legacy_shards[index] = None;
|
|
rustfs_shards[index] = None;
|
|
}
|
|
|
|
let legacy = LegacyEcDecodeEngine::new(erasure.clone());
|
|
let rustfs = RustfsCodecDecodeEngine::new(&erasure).expect("engine should be created");
|
|
|
|
assert!(reconstruct_with(&legacy, &mut legacy_shards).is_err());
|
|
assert!(reconstruct_with(&rustfs, &mut rustfs_shards).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn rustfs_codec_decode_engine_recovers_missing_data_and_parity() {
|
|
// backlog#868 item 2: one missing data shard plus one missing parity
|
|
// shard is recoverable, but the parity verification path used to
|
|
// require every shard slot to be present after data-only
|
|
// reconstruction and misreported this degraded read as a failure.
|
|
let erasure = Erasure::new(6, 4, 96);
|
|
let mut shards = encoded_shards(&erasure, &(0u8..=191u8).collect::<Vec<_>>());
|
|
let expected_data = shards.iter().take(erasure.data_shards).cloned().collect::<Vec<_>>();
|
|
shards[1] = None;
|
|
shards[erasure.data_shards + 1] = None;
|
|
|
|
let engine = RustfsCodecDecodeEngine::new(&erasure).expect("engine should be created");
|
|
reconstruct_with(&engine, &mut shards).expect("one missing data shard plus one missing parity shard is recoverable");
|
|
|
|
assert_eq!(shards.iter().take(erasure.data_shards).cloned().collect::<Vec<_>>(), expected_data);
|
|
assert!(shards[erasure.data_shards + 1].is_some(), "missing parity is rebuilt for verification");
|
|
}
|
|
|
|
#[test]
|
|
fn rustfs_codec_decode_engine_matches_legacy_on_missing_data_and_parity() {
|
|
let erasure = Erasure::new(6, 4, 96);
|
|
let mut legacy_shards = encoded_shards(&erasure, &(0u8..=191u8).rev().collect::<Vec<_>>());
|
|
let mut rustfs_shards = legacy_shards.clone();
|
|
for shards in [&mut legacy_shards, &mut rustfs_shards] {
|
|
shards[2] = None;
|
|
shards[erasure.data_shards] = None;
|
|
}
|
|
|
|
let legacy = LegacyEcDecodeEngine::new(erasure.clone());
|
|
let rustfs = RustfsCodecDecodeEngine::new(&erasure).expect("engine should be created");
|
|
|
|
reconstruct_with(&legacy, &mut legacy_shards).expect("legacy should recover missing data plus parity");
|
|
reconstruct_with(&rustfs, &mut rustfs_shards).expect("rustfs codec should recover missing data plus parity");
|
|
|
|
assert_eq!(
|
|
rustfs_shards.iter().take(erasure.data_shards).cloned().collect::<Vec<_>>(),
|
|
legacy_shards.iter().take(erasure.data_shards).cloned().collect::<Vec<_>>()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rustfs_codec_decode_engine_rejects_corrupt_parity_with_missing_data_and_parity() {
|
|
// The degraded-read allowance must not weaken inconsistent-source
|
|
// rejection: with one data and one parity shard missing, a corrupted
|
|
// surviving parity shard still fails verification.
|
|
let erasure = Erasure::new(6, 4, 96);
|
|
let mut shards = encoded_shards(&erasure, &(0u8..=191u8).collect::<Vec<_>>());
|
|
shards[0] = None;
|
|
shards[erasure.data_shards] = None;
|
|
let Some(corrupt_parity) = shards[erasure.data_shards + 1].as_mut() else {
|
|
panic!("test parity shard should be present");
|
|
};
|
|
corrupt_parity[0] ^= 0x80;
|
|
|
|
let engine = RustfsCodecDecodeEngine::new(&erasure).expect("engine should be created");
|
|
let err = reconstruct_with(&engine, &mut shards).expect_err("corrupt surviving parity must still be rejected");
|
|
|
|
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
|
|
assert!(err.to_string().contains("inconsistent read source shards"));
|
|
}
|
|
|
|
#[test]
|
|
fn rustfs_codec_decode_engine_rejects_stale_data_with_missing_data_and_parity() {
|
|
let erasure = Erasure::new(6, 4, 96);
|
|
let mut shards = encoded_shards(&erasure, &(0u8..=191u8).collect::<Vec<_>>());
|
|
shards[0] = None;
|
|
shards[erasure.data_shards] = None;
|
|
let Some(stale_data) = shards[1].as_mut() else {
|
|
panic!("test data shard should be present");
|
|
};
|
|
stale_data[0] ^= 0x40;
|
|
|
|
let engine = RustfsCodecDecodeEngine::new(&erasure).expect("engine should be created");
|
|
let err = reconstruct_with(&engine, &mut shards).expect_err("stale surviving data must still be rejected");
|
|
|
|
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
|
|
assert!(err.to_string().contains("inconsistent read source shards"));
|
|
}
|
|
|
|
#[test]
|
|
fn rustfs_codec_decode_engine_rejects_inconsistent_reconstruction_sources() {
|
|
let erasure = Erasure::new(2, 2, 32);
|
|
let encoded = erasure
|
|
.encode_data(&(0u8..64u8).collect::<Vec<_>>())
|
|
.expect("test stripe should encode");
|
|
let mut shards = encoded.into_iter().map(|shard| Some(shard.to_vec())).collect::<Vec<_>>();
|
|
shards[0] = None;
|
|
let Some(corrupt_parity) = shards[erasure.data_shards].as_mut() else {
|
|
panic!("test parity shard should be present");
|
|
};
|
|
corrupt_parity[0] ^= 0x80;
|
|
|
|
let engine = RustfsCodecDecodeEngine::new(&erasure).expect("engine should be created");
|
|
let err = reconstruct_with(&engine, &mut shards).expect_err("rustfs codec should reject inconsistent sources");
|
|
|
|
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
|
|
assert!(err.to_string().contains("inconsistent read source shards"));
|
|
}
|
|
|
|
#[test]
|
|
fn rustfs_codec_decode_engine_rejects_stale_data_source() {
|
|
let erasure = Erasure::new(4, 2, 32);
|
|
let encoded = erasure
|
|
.encode_data(&(0u8..128u8).collect::<Vec<_>>())
|
|
.expect("test stripe should encode");
|
|
let mut shards = encoded.into_iter().map(|shard| Some(shard.to_vec())).collect::<Vec<_>>();
|
|
shards[0] = None;
|
|
let Some(stale_data) = shards[1].as_mut() else {
|
|
panic!("test data shard should be present");
|
|
};
|
|
stale_data[0] ^= 0x40;
|
|
|
|
let engine = RustfsCodecDecodeEngine::new(&erasure).expect("engine should be created");
|
|
let err = reconstruct_with(&engine, &mut shards).expect_err("rustfs codec should reject stale data sources");
|
|
|
|
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
|
|
assert!(err.to_string().contains("inconsistent read source shards"));
|
|
}
|
|
|
|
#[test]
|
|
fn rustfs_codec_decode_engine_rebuilds_missing_parity_for_source_verification() {
|
|
let erasure = Erasure::new(2, 3, 32);
|
|
let encoded = erasure
|
|
.encode_data(&(0u8..64u8).collect::<Vec<_>>())
|
|
.expect("test stripe should encode");
|
|
let mut shards = encoded.into_iter().map(|shard| Some(shard.to_vec())).collect::<Vec<_>>();
|
|
shards[0] = None;
|
|
let missing_parity_index = erasure.data_shards + erasure.parity_shards - 1;
|
|
shards[missing_parity_index] = None;
|
|
|
|
let engine = RustfsCodecDecodeEngine::new(&erasure).expect("engine should be created");
|
|
let mut workspace = engine
|
|
.prepare_workspace(erasure.shard_size())
|
|
.expect("workspace should be prepared");
|
|
let outcome = engine
|
|
.reconstruct_into(&mut shards, &mut workspace)
|
|
.expect("missing data plus missing parity should be recoverable");
|
|
|
|
assert_eq!(outcome, GET_RECONSTRUCT_OUTCOME_RUSTFS_CALLED);
|
|
assert!(shards[0].is_some());
|
|
assert!(shards[missing_parity_index].is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn rustfs_codec_decode_engine_rejects_invalid_empty_payload_shape() {
|
|
let erasure = Erasure::new(2, 2, 16);
|
|
let engine = RustfsCodecDecodeEngine::new(&erasure).expect("engine should be created");
|
|
let mut workspace = engine.prepare_workspace(0).expect("workspace should be prepared");
|
|
let mut shards = vec![Some(Vec::new()), None, Some(Vec::new())];
|
|
|
|
let err = engine
|
|
.reconstruct_into(&mut shards, &mut workspace)
|
|
.expect_err("invalid empty payload shard count must fail closed");
|
|
|
|
assert!(err.to_string().contains("invalid shard count"));
|
|
}
|
|
|
|
#[test]
|
|
fn rustfs_codec_decode_engine_skips_empty_payload_when_enough_empty_sources_exist() {
|
|
let erasure = Erasure::new(3, 2, 16);
|
|
let engine = RustfsCodecDecodeEngine::new(&erasure).expect("engine should be created");
|
|
let mut workspace = engine.prepare_workspace(0).expect("workspace should be prepared");
|
|
let mut shards = vec![Some(Vec::new()), None, Some(Vec::new()), Some(Vec::new()), None];
|
|
|
|
let outcome = engine
|
|
.reconstruct_into(&mut shards, &mut workspace)
|
|
.expect("empty payload should be restored without codec allocation");
|
|
|
|
assert_eq!(outcome, GET_RECONSTRUCT_OUTCOME_SKIP_EMPTY_PAYLOAD);
|
|
assert_eq!(shards[1], Some(Vec::new()));
|
|
assert!(!engine.codec_is_initialized());
|
|
}
|
|
|
|
#[test]
|
|
fn rustfs_codec_decode_engine_returns_called_without_parity_codec() {
|
|
let erasure = Erasure::new(2, 0, 16);
|
|
let engine = RustfsCodecDecodeEngine::new(&erasure).expect("engine should be created");
|
|
let mut workspace = engine.prepare_workspace(4).expect("workspace should be prepared");
|
|
let mut shards = vec![None, Some(vec![2, 2, 2, 2])];
|
|
|
|
let outcome = engine
|
|
.reconstruct_into(&mut shards, &mut workspace)
|
|
.expect("zero parity engine should not allocate codec");
|
|
|
|
assert_eq!(outcome, GET_RECONSTRUCT_OUTCOME_RUSTFS_CALLED);
|
|
assert!(shards[0].is_none());
|
|
assert!(!engine.codec_is_initialized());
|
|
}
|
|
|
|
#[test]
|
|
fn rustfs_codec_decode_engine_recovers_empty_data_shard() {
|
|
let erasure = Erasure::new(4, 2, 16);
|
|
let mut shards = encoded_shards(&erasure, b"");
|
|
shards[0] = None;
|
|
|
|
let engine = RustfsCodecDecodeEngine::new(&erasure).expect("engine should be created");
|
|
reconstruct_with(&engine, &mut shards).expect("empty shard should reconstruct");
|
|
|
|
assert_eq!(shards[0], Some(Vec::new()));
|
|
assert!(shards.iter().take(erasure.data_shards).all(Option::is_some));
|
|
}
|
|
|
|
#[test]
|
|
fn codec_streaming_decode_engine_dispatches_to_legacy_workspace_and_rejects_mismatch() {
|
|
let erasure = Erasure::new(2, 2, 16);
|
|
let mut shards = encoded_shards(&erasure, b"legacy enum dispatch");
|
|
shards[0] = None;
|
|
|
|
let engine = CodecStreamingDecodeEngine::legacy(erasure);
|
|
assert_eq!(engine.data_shards(), 2);
|
|
assert_eq!(engine.parity_shards(), 2);
|
|
assert_eq!(engine.block_size(), 16);
|
|
assert_eq!(engine.engine_name(), GET_CODEC_STREAMING_ENGINE_LEGACY);
|
|
assert!(!engine.supports_progressive_decode());
|
|
assert!(!engine.supports_aligned_shards());
|
|
|
|
let mut workspace = engine.prepare_workspace(8).expect("workspace should be prepared");
|
|
assert_eq!(workspace.shard_len(), 8);
|
|
engine
|
|
.reconstruct_into(&mut shards, &mut workspace)
|
|
.expect("legacy enum engine should dispatch reconstruction");
|
|
assert!(shards.iter().take(engine.data_shards()).all(Option::is_some));
|
|
|
|
let rustfs = CodecStreamingDecodeEngine::rustfs(&Erasure::new(2, 2, 16)).expect("engine should be created");
|
|
let err = rustfs
|
|
.reconstruct_into(&mut shards, &mut workspace)
|
|
.expect_err("engine/workspace variant mismatch must fail");
|
|
assert!(err.to_string().contains("engine/workspace mismatch"));
|
|
}
|
|
|
|
#[test]
|
|
fn codec_streaming_decode_engine_dispatches_to_rustfs_workspace() {
|
|
let erasure = Erasure::new(4, 2, 16);
|
|
let mut shards = encoded_shards(&erasure, b"dispatch keeps rustfs engine byte-identical");
|
|
shards[2] = None;
|
|
|
|
let engine = CodecStreamingDecodeEngine::rustfs(&erasure).expect("engine should be created");
|
|
let mut workspace = engine.prepare_workspace(4).expect("workspace should be prepared");
|
|
if let CodecStreamingDecodeWorkspace::Rustfs(workspace) = &workspace {
|
|
assert_eq!(<RustfsCodecDecodeWorkspace as DecodeWorkspace>::shard_len(workspace), 4);
|
|
}
|
|
engine
|
|
.reconstruct_into(&mut shards, &mut workspace)
|
|
.expect("enum engine should dispatch reconstruction");
|
|
|
|
assert!(shards.iter().take(engine.data_shards()).all(Option::is_some));
|
|
}
|
|
}
|