mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-08 14:23:13 +00:00
46d7f9e1f2
* feat(get): consolidate GET performance optimization Consolidated implementation of all GET performance optimizations into a single, well-organized commit replacing the previous patch-on-patch approach. ## Changes ### Configuration (set_disk/mod.rs) - Consolidated all GET optimization flags into a single organized section - Enabled by default: codec streaming, metadata early-stop, page cache reclaim - Added codec streaming multipart flag (default: disabled) - Added version-aware early-stop flag (default: disabled) - Added adaptive duplex buffer sizing based on object size - All flags use OnceLock caching with rollout percentage support ### Metadata Early-Stop (set_disk/read.rs) - Delete marker early-stop when quorum agrees - Version-aware early-stop for versioned GET requests - MetadataQuorumAccumulator enhanced with: - delete_marker_votes tracking - requested_version_id and matching_version_votes tracking - version_early_stop_decision() method - 6 new tests for version early-stop scenarios ### Codec Streaming (erasure/coding/decode_reader.rs) - DualInFlight (2-stripe lookahead) enabled by default ### Decode Pipeline (erasure/coding/decode.rs) - Stripe prefetch count configuration - Bitrot-decode overlap configuration ### Disk Layer (disk/local.rs) - O_DIRECT read configuration constants (preparation) ### Metrics (io-metrics/lib.rs) - BytesPool acquisition/return metrics - Metadata phase duration with early-stop label - Total duration with reader_path label ### Diagnostics (diagnostics/) - Early-stop reason constants - Pool tier/outcome label constants ### Observability (.docker/observability/) - 3 Grafana dashboards for GET optimization monitoring - Prometheus alert rules (6 alerts: 3 critical, 3 warning) - Updated README.md and README_ZH.md with usage docs ### Config (config/src/constants/runtime.rs) - Page cache reclaim read enabled by default ## Environment Variables | Variable | Default | Description | |----------|---------|-------------| | RUSTFS_GET_CODEC_STREAMING_ENABLE | true | Codec streaming base flag | | RUSTFS_GET_CODEC_STREAMING_ROLLOUT_PCT | 100 | Codec streaming rollout % | | RUSTFS_GET_CODEC_STREAMING_MULTIPART_ENABLE | false | Multipart codec streaming | | RUSTFS_GET_METADATA_EARLY_STOP_ENABLE | true | Early-stop base flag | | RUSTFS_GET_METADATA_EARLY_STOP_ROLLOUT_PCT | 100 | Early-stop rollout % | | RUSTFS_GET_METADATA_VERSION_EARLY_STOP_ENABLE | false | Version-aware early-stop | | RUSTFS_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE | true | Page cache reclaim | | RUSTFS_OBJECT_DIRECT_IO_READ_ENABLE | false | O_DIRECT (preparation) | | RUSTFS_GET_DECODE_STRIPE_PREFETCH_COUNT | 1 | Stripe prefetch | | RUSTFS_GET_BITROT_DECODE_OVERLAP_ENABLE | false | Bitrot-decode overlap | | RUSTFS_GET_CODEC_STREAMING_MAX_INFLIGHT | 2 | DualInFlight stripes | ## Rollback All optimizations can be disabled via environment variables: RUSTFS_GET_CODEC_STREAMING_ENABLE=false RUSTFS_GET_METADATA_EARLY_STOP_ENABLE=false RUSTFS_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE=false Co-Authored-By: heihutu <heihutu@gmail.com> * test(get): add stress test scripts for GET optimization validation - quick-validate-get-optimization.sh: Quick 5-minute validation - stress-test-get-optimization.sh: Full 30+ minute stress test - README-stress-test.md: Usage documentation Co-Authored-By: heihutu <heihutu@gmail.com> * test(ecstore): align file cache reclaim defaults * chore(deps): update redis and erasure codec * test(ecstore): align decode fill policy default * fix(get): wire codec streaming rollout gate * perf(get): skip metrics-off codec timers * test(get): capture codec streaming diagnostics * test(get): add multipart fallback probe * test(get): add encrypted fallback probe * test(get): add compressed fallback probe * test(get): add degraded read fallback probe * test(get): cover remote fallback probe * test(get): report warp request p99 * test(get): capture OTLP metric deltas * perf(get): align codec streaming inflight default * perf(get): reuse codec reader output buffers * test(get): count codec reader fill starts * perf(get): reuse codec reader fill worker * perf(get): lazy init rustfs codec reconstruct * test(get): cover rustfs codec source faults * docs(get): record rustfs codec fallback scope * feat(get): add multipart codec reader opt-in * test(get): add multipart codec smoke option * test(get): cover multipart codec degraded fallback * perf(get): bound multipart codec eager setup * test(get): satisfy codec hardening PR gate --------- Co-authored-by: heihutu <heihutu@gmail.com>
1528 lines
55 KiB
Rust
1528 lines
55 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::diagnostics::get::{
|
|
GET_OBJECT_PATH_CODEC_STREAMING, GET_READER_BUFFER_OUTPUT, GET_READER_BUFFER_PREFETCH, GET_READER_POLL_PENDING,
|
|
GET_READER_POLL_READY_DATA, GET_READER_POLL_READY_EMPTY, GET_READER_POLL_READY_ERROR, GET_READER_PREFETCH_DIRECT,
|
|
GET_READER_PREFETCH_EOF, GET_READER_PREFETCH_ERROR_DEFERRED, GET_READER_PREFETCH_ERROR_IMMEDIATE, GET_READER_PREFETCH_STORED,
|
|
GET_STAGE_DECODE, GET_STAGE_EMIT, GET_STAGE_FILL, GET_STAGE_OUTPUT_LOCK_WAIT, GET_STAGE_OUTPUT_POLL, GET_STAGE_RECONSTRUCT,
|
|
GET_STAGE_STRIPE_READ, get_stage_timer_if_enabled, record_get_stage_duration_if_enabled,
|
|
};
|
|
use crate::disk::error::Error as DiskError;
|
|
use crate::erasure::codec::bridge::{ErasureDecodeEngine, GET_RECONSTRUCT_OUTCOME_SKIP_DATA_COMPLETE};
|
|
use crate::set_disk::shard_source::{ShardStripeSource, StripeReadState};
|
|
use std::collections::VecDeque;
|
|
use std::io;
|
|
use std::io::ErrorKind;
|
|
use std::pin::Pin;
|
|
use std::sync::Mutex;
|
|
use std::task::{Context, Poll, ready};
|
|
use std::time::Instant;
|
|
use tokio::io::{AsyncRead, ReadBuf};
|
|
use tokio::sync::{mpsc, oneshot};
|
|
use tokio::task::JoinHandle;
|
|
|
|
const ENV_RUSTFS_GET_CODEC_STREAMING_MAX_INFLIGHT: &str = "RUSTFS_GET_CODEC_STREAMING_MAX_INFLIGHT";
|
|
const DEFAULT_RUSTFS_GET_CODEC_STREAMING_MAX_INFLIGHT: usize = 2;
|
|
const FILL_POLICY_SINGLE_INFLIGHT: &str = "single_inflight";
|
|
const FILL_POLICY_DUAL_INFLIGHT: &str = "dual_inflight";
|
|
|
|
type FillTask = oneshot::Receiver<FillResult>;
|
|
|
|
struct FillWorker {
|
|
tx: mpsc::Sender<FillRequest>,
|
|
task: JoinHandle<()>,
|
|
}
|
|
|
|
struct FillRequest {
|
|
remaining: usize,
|
|
reusable_buffers: Vec<Vec<u8>>,
|
|
response: oneshot::Sender<FillResult>,
|
|
}
|
|
|
|
struct FillResult {
|
|
result: io::Result<Option<Vec<u8>>>,
|
|
queued_buffers: VecDeque<Vec<u8>>,
|
|
reusable_buffers: Vec<Vec<u8>>,
|
|
deferred_error: Option<io::Error>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
enum FillPolicy {
|
|
SingleInFlight,
|
|
DualInFlight,
|
|
}
|
|
|
|
impl FillPolicy {
|
|
fn from_env() -> Self {
|
|
match rustfs_utils::get_env_usize(
|
|
ENV_RUSTFS_GET_CODEC_STREAMING_MAX_INFLIGHT,
|
|
DEFAULT_RUSTFS_GET_CODEC_STREAMING_MAX_INFLIGHT,
|
|
) {
|
|
2 => Self::DualInFlight,
|
|
_ => Self::SingleInFlight,
|
|
}
|
|
}
|
|
|
|
const fn max_inflight(self) -> usize {
|
|
match self {
|
|
Self::SingleInFlight => 1,
|
|
Self::DualInFlight => 2,
|
|
}
|
|
}
|
|
|
|
const fn additional_queued_buffers(self) -> usize {
|
|
self.max_inflight().saturating_sub(1)
|
|
}
|
|
|
|
const fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::SingleInFlight => FILL_POLICY_SINGLE_INFLIGHT,
|
|
Self::DualInFlight => FILL_POLICY_DUAL_INFLIGHT,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) struct ErasureDecodeReader<S, E>
|
|
where
|
|
E: ErasureDecodeEngine,
|
|
{
|
|
metrics_path: &'static str,
|
|
stage_metrics_enabled: bool,
|
|
fill_policy: FillPolicy,
|
|
source: Option<S>,
|
|
engine: Option<E>,
|
|
workspace: Option<E::Workspace>,
|
|
worker: Option<FillWorker>,
|
|
output_buf: Vec<u8>,
|
|
output_pos: usize,
|
|
reusable_output_bufs: Vec<Vec<u8>>,
|
|
prefetched_bufs: VecDeque<Vec<u8>>,
|
|
prefetch_error: Option<io::Error>,
|
|
prefetch_wait_started_at: Option<Instant>,
|
|
output_wait_started_at: Option<Instant>,
|
|
remaining: usize,
|
|
// Bounded lookahead controlled by `FillPolicy`.
|
|
fill: Option<FillTask>,
|
|
}
|
|
|
|
impl<S, E> ErasureDecodeReader<S, E>
|
|
where
|
|
S: ShardStripeSource + Send + 'static,
|
|
E: ErasureDecodeEngine + Clone + Send + Sync + 'static,
|
|
{
|
|
pub(crate) fn new(source: S, engine: E, total_length: usize) -> io::Result<Self> {
|
|
Self::new_with_metrics_path(source, engine, total_length, GET_OBJECT_PATH_CODEC_STREAMING)
|
|
}
|
|
|
|
pub(crate) fn new_with_metrics_path(
|
|
source: S,
|
|
engine: E,
|
|
total_length: usize,
|
|
metrics_path: &'static str,
|
|
) -> io::Result<Self> {
|
|
Self::new_with_fill_policy_inner(source, engine, total_length, metrics_path, FillPolicy::from_env())
|
|
}
|
|
|
|
fn new_with_fill_policy_inner(
|
|
source: S,
|
|
engine: E,
|
|
total_length: usize,
|
|
metrics_path: &'static str,
|
|
fill_policy: FillPolicy,
|
|
) -> io::Result<Self> {
|
|
if engine.data_shards() == 0 {
|
|
return Err(io::Error::new(ErrorKind::InvalidInput, "erasure reader requires data shards"));
|
|
}
|
|
if engine.block_size() == 0 {
|
|
return Err(io::Error::new(ErrorKind::InvalidInput, "erasure reader requires non-zero block size"));
|
|
}
|
|
|
|
let shard_len = engine.block_size().div_ceil(engine.data_shards());
|
|
let workspace = engine.prepare_workspace(shard_len)?;
|
|
|
|
Ok(Self {
|
|
metrics_path,
|
|
stage_metrics_enabled: rustfs_io_metrics::get_stage_metrics_enabled(),
|
|
fill_policy,
|
|
source: Some(source),
|
|
engine: Some(engine),
|
|
workspace: Some(workspace),
|
|
worker: None,
|
|
output_buf: Vec::new(),
|
|
output_pos: 0,
|
|
reusable_output_bufs: Vec::new(),
|
|
prefetched_bufs: VecDeque::new(),
|
|
prefetch_error: None,
|
|
prefetch_wait_started_at: None,
|
|
output_wait_started_at: None,
|
|
remaining: total_length,
|
|
fill: None,
|
|
})
|
|
}
|
|
|
|
fn max_reusable_output_bufs(&self) -> usize {
|
|
self.fill_policy.max_inflight() + 1
|
|
}
|
|
|
|
fn push_reusable_output_buf(&mut self, mut buf: Vec<u8>) {
|
|
if buf.capacity() == 0 || self.reusable_output_bufs.len() >= self.max_reusable_output_bufs() {
|
|
return;
|
|
}
|
|
buf.clear();
|
|
self.reusable_output_bufs.push(buf);
|
|
}
|
|
|
|
fn recycle_drained_output_buf(&mut self) {
|
|
if self.output_pos < self.output_buf.len() {
|
|
return;
|
|
}
|
|
|
|
let buf = std::mem::take(&mut self.output_buf);
|
|
self.output_pos = 0;
|
|
self.push_reusable_output_buf(buf);
|
|
}
|
|
|
|
fn extend_reusable_output_bufs(&mut self, bufs: Vec<Vec<u8>>) {
|
|
for buf in bufs {
|
|
self.push_reusable_output_buf(buf);
|
|
}
|
|
}
|
|
|
|
fn fill_worker_tx(&mut self) -> io::Result<mpsc::Sender<FillRequest>> {
|
|
if self.worker.is_none() {
|
|
let Some(source) = self.source.take() else {
|
|
return Err(io::Error::new(ErrorKind::BrokenPipe, "erasure reader source missing"));
|
|
};
|
|
let Some(engine) = self.engine.take() else {
|
|
self.source = Some(source);
|
|
return Err(io::Error::new(ErrorKind::BrokenPipe, "erasure reader engine missing"));
|
|
};
|
|
let Some(workspace) = self.workspace.take() else {
|
|
self.source = Some(source);
|
|
self.engine = Some(engine);
|
|
return Err(io::Error::new(ErrorKind::BrokenPipe, "erasure reader workspace missing"));
|
|
};
|
|
|
|
let (tx, rx) = mpsc::channel(1);
|
|
rustfs_io_metrics::record_get_object_fill_worker_started(self.metrics_path, self.fill_policy.as_str());
|
|
let task = tokio::spawn(run_fill_worker(
|
|
source,
|
|
engine,
|
|
workspace,
|
|
self.fill_policy,
|
|
self.metrics_path,
|
|
self.stage_metrics_enabled,
|
|
rx,
|
|
));
|
|
self.worker = Some(FillWorker { tx, task });
|
|
}
|
|
|
|
self.worker
|
|
.as_ref()
|
|
.map(|worker| worker.tx.clone())
|
|
.ok_or_else(|| io::Error::new(ErrorKind::BrokenPipe, "erasure reader fill worker missing"))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn new_with_fill_policy(
|
|
source: S,
|
|
engine: E,
|
|
total_length: usize,
|
|
metrics_path: &'static str,
|
|
fill_policy: FillPolicy,
|
|
) -> io::Result<Self> {
|
|
Self::new_with_fill_policy_inner(source, engine, total_length, metrics_path, fill_policy)
|
|
}
|
|
|
|
fn poll_fill_result(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<Option<Vec<u8>>>> {
|
|
if self.fill.is_none() {
|
|
let fill_worker_tx = match self.fill_worker_tx() {
|
|
Ok(tx) => tx,
|
|
Err(err) => return Poll::Ready(Err(err)),
|
|
};
|
|
let metrics_path = self.metrics_path;
|
|
let fill_policy = self.fill_policy;
|
|
let remaining = self.remaining;
|
|
let reusable_buffers = std::mem::take(&mut self.reusable_output_bufs);
|
|
let (response, fill) = oneshot::channel();
|
|
let request = FillRequest {
|
|
remaining,
|
|
reusable_buffers,
|
|
response,
|
|
};
|
|
|
|
if let Err(err) = fill_worker_tx.try_send(request) {
|
|
self.extend_reusable_output_bufs(err.into_inner().reusable_buffers);
|
|
return Poll::Ready(Err(io::Error::new(
|
|
ErrorKind::BrokenPipe,
|
|
"erasure reader fill worker request queue is closed or full",
|
|
)));
|
|
}
|
|
|
|
rustfs_io_metrics::record_get_object_fill_started(metrics_path, fill_policy.as_str());
|
|
self.fill = Some(fill);
|
|
}
|
|
|
|
let fill = self
|
|
.fill
|
|
.as_mut()
|
|
.ok_or_else(|| io::Error::new(ErrorKind::BrokenPipe, "erasure reader fill future missing"))?;
|
|
let fill_result = ready!(Pin::new(fill).poll(cx));
|
|
let FillResult {
|
|
result,
|
|
queued_buffers,
|
|
reusable_buffers,
|
|
deferred_error,
|
|
} = match fill_result {
|
|
Ok(result) => result,
|
|
Err(err) => {
|
|
self.fill = None;
|
|
return Poll::Ready(Err(io::Error::other(format!("erasure reader fill worker stopped: {err}"))));
|
|
}
|
|
};
|
|
|
|
self.fill = None;
|
|
self.extend_reusable_output_bufs(reusable_buffers);
|
|
if let Some(deferred_error) = deferred_error {
|
|
self.prefetch_error = Some(deferred_error);
|
|
}
|
|
self.prefetched_bufs.extend(queued_buffers);
|
|
|
|
match result {
|
|
Ok(Some(buf)) => {
|
|
if buf.is_empty() && self.remaining > 0 {
|
|
return Poll::Ready(Err(DiskError::LessData.into()));
|
|
}
|
|
rustfs_io_metrics::record_get_object_reader_stripe(self.metrics_path);
|
|
rustfs_io_metrics::record_get_object_reader_bytes(self.metrics_path, buf.len());
|
|
self.remaining -= buf.len();
|
|
Poll::Ready(Ok(Some(buf)))
|
|
}
|
|
Ok(None) => {
|
|
if self.remaining == 0 {
|
|
Poll::Ready(Ok(None))
|
|
} else {
|
|
Poll::Ready(Err(DiskError::LessData.into()))
|
|
}
|
|
}
|
|
Err(err) => Poll::Ready(Err(err)),
|
|
}
|
|
}
|
|
|
|
fn poll_prefetch(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
|
if self.prefetched_bufs.len() >= self.fill_policy.max_inflight() || self.prefetch_error.is_some() || self.remaining == 0 {
|
|
return Poll::Ready(Ok(()));
|
|
}
|
|
|
|
if self.stage_metrics_enabled && self.prefetch_wait_started_at.is_none() {
|
|
self.prefetch_wait_started_at = Some(Instant::now());
|
|
}
|
|
|
|
let fill = match self.poll_fill_result(cx) {
|
|
Poll::Ready(result) => {
|
|
if self.stage_metrics_enabled
|
|
&& let Some(started_at) = self.prefetch_wait_started_at.take()
|
|
{
|
|
rustfs_io_metrics::record_get_object_reader_prefetch_wait(
|
|
self.metrics_path,
|
|
started_at.elapsed().as_secs_f64(),
|
|
);
|
|
}
|
|
result
|
|
}
|
|
Poll::Pending => return Poll::Pending,
|
|
};
|
|
|
|
match fill {
|
|
Ok(Some(buf)) => {
|
|
let output_has_remaining = self.output_pos < self.output_buf.len();
|
|
let mut queued_count = 0usize;
|
|
let mut queued_bytes = 0usize;
|
|
|
|
if output_has_remaining {
|
|
rustfs_io_metrics::record_get_object_fill_completed_before_output_drained(
|
|
self.metrics_path,
|
|
self.fill_policy.as_str(),
|
|
);
|
|
queued_bytes += buf.len();
|
|
queued_count += 1;
|
|
self.prefetched_bufs.push_back(buf);
|
|
} else {
|
|
rustfs_io_metrics::record_get_object_reader_prefetch(self.metrics_path, GET_READER_PREFETCH_DIRECT);
|
|
rustfs_io_metrics::record_get_object_reader_buffer(self.metrics_path, GET_READER_BUFFER_OUTPUT, buf.len());
|
|
self.output_buf = buf;
|
|
self.output_pos = 0;
|
|
}
|
|
|
|
if !self.prefetched_bufs.is_empty() {
|
|
let total_prefetched_bytes = self.prefetched_bufs.iter().map(Vec::len).sum::<usize>();
|
|
if total_prefetched_bytes > queued_bytes {
|
|
queued_bytes = total_prefetched_bytes;
|
|
}
|
|
if self.prefetched_bufs.len() > queued_count {
|
|
queued_count = self.prefetched_bufs.len();
|
|
}
|
|
}
|
|
if queued_count > 0 {
|
|
rustfs_io_metrics::record_get_object_fill_queued(self.metrics_path, self.fill_policy.as_str(), queued_count);
|
|
rustfs_io_metrics::record_get_object_reader_prefetch_bytes(
|
|
self.metrics_path,
|
|
self.fill_policy.as_str(),
|
|
queued_bytes,
|
|
);
|
|
rustfs_io_metrics::record_get_object_reader_prefetch(self.metrics_path, GET_READER_PREFETCH_STORED);
|
|
rustfs_io_metrics::record_get_object_reader_buffer(
|
|
self.metrics_path,
|
|
GET_READER_BUFFER_PREFETCH,
|
|
queued_bytes,
|
|
);
|
|
}
|
|
Poll::Ready(Ok(()))
|
|
}
|
|
Ok(None) => {
|
|
rustfs_io_metrics::record_get_object_reader_prefetch(self.metrics_path, GET_READER_PREFETCH_EOF);
|
|
Poll::Ready(Ok(()))
|
|
}
|
|
Err(err) => {
|
|
if self.output_pos < self.output_buf.len() {
|
|
rustfs_io_metrics::record_get_object_reader_prefetch(self.metrics_path, GET_READER_PREFETCH_ERROR_DEFERRED);
|
|
self.prefetch_error = Some(err);
|
|
Poll::Ready(Ok(()))
|
|
} else {
|
|
rustfs_io_metrics::record_get_object_reader_prefetch(self.metrics_path, GET_READER_PREFETCH_ERROR_IMMEDIATE);
|
|
Poll::Ready(Err(err))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn run_fill_worker<S, E>(
|
|
mut source: S,
|
|
engine: E,
|
|
mut workspace: E::Workspace,
|
|
fill_policy: FillPolicy,
|
|
metrics_path: &'static str,
|
|
stage_metrics_enabled: bool,
|
|
mut rx: mpsc::Receiver<FillRequest>,
|
|
) where
|
|
S: ShardStripeSource + Send + 'static,
|
|
E: ErasureDecodeEngine + Send + Sync + 'static,
|
|
{
|
|
while let Some(request) = rx.recv().await {
|
|
let response = request.response;
|
|
let result = run_fill_request(FillRequestWork {
|
|
source: &mut source,
|
|
engine: &engine,
|
|
workspace: &mut workspace,
|
|
fill_policy,
|
|
metrics_path,
|
|
stage_metrics_enabled,
|
|
remaining: request.remaining,
|
|
reusable_buffers: request.reusable_buffers,
|
|
})
|
|
.await;
|
|
let _ = response.send(result);
|
|
}
|
|
}
|
|
|
|
struct FillRequestWork<'a, S, E>
|
|
where
|
|
E: ErasureDecodeEngine,
|
|
{
|
|
source: &'a mut S,
|
|
engine: &'a E,
|
|
workspace: &'a mut E::Workspace,
|
|
fill_policy: FillPolicy,
|
|
metrics_path: &'static str,
|
|
stage_metrics_enabled: bool,
|
|
remaining: usize,
|
|
reusable_buffers: Vec<Vec<u8>>,
|
|
}
|
|
|
|
async fn run_fill_request<S, E>(work: FillRequestWork<'_, S, E>) -> FillResult
|
|
where
|
|
S: ShardStripeSource + Send,
|
|
E: ErasureDecodeEngine,
|
|
{
|
|
let FillRequestWork {
|
|
source,
|
|
engine,
|
|
workspace,
|
|
fill_policy,
|
|
metrics_path,
|
|
stage_metrics_enabled,
|
|
remaining,
|
|
mut reusable_buffers,
|
|
} = work;
|
|
let mut queued_buffers = VecDeque::new();
|
|
let mut deferred_error = None;
|
|
let fill_stage_start = get_stage_timer_if_enabled(stage_metrics_enabled);
|
|
let stripe_read_stage_start = get_stage_timer_if_enabled(stage_metrics_enabled);
|
|
let state = source.read_next_stripe().await;
|
|
record_get_stage_duration_if_enabled(metrics_path, GET_STAGE_STRIPE_READ, stripe_read_stage_start);
|
|
let decode_stage_start = get_stage_timer_if_enabled(stage_metrics_enabled);
|
|
let mut output_buf = reusable_buffers.pop().unwrap_or_default();
|
|
let result =
|
|
match decode_stripe_into(metrics_path, stage_metrics_enabled, engine, workspace, state, remaining, &mut output_buf) {
|
|
Ok(true) => Ok(Some(output_buf)),
|
|
Ok(false) => {
|
|
reusable_buffers.push(output_buf);
|
|
Ok(None)
|
|
}
|
|
Err(err) => {
|
|
reusable_buffers.push(output_buf);
|
|
Err(err)
|
|
}
|
|
};
|
|
record_get_stage_duration_if_enabled(metrics_path, GET_STAGE_DECODE, decode_stage_start);
|
|
if let Ok(Some(first_buf)) = result.as_ref() {
|
|
let mut remaining_after_first = remaining.saturating_sub(first_buf.len());
|
|
for _ in 0..fill_policy.additional_queued_buffers() {
|
|
if remaining_after_first == 0 {
|
|
break;
|
|
}
|
|
let stripe_read_stage_start = get_stage_timer_if_enabled(stage_metrics_enabled);
|
|
let state = source.read_next_stripe().await;
|
|
record_get_stage_duration_if_enabled(metrics_path, GET_STAGE_STRIPE_READ, stripe_read_stage_start);
|
|
let decode_stage_start = get_stage_timer_if_enabled(stage_metrics_enabled);
|
|
let mut queued_buf = reusable_buffers.pop().unwrap_or_default();
|
|
let queued_result = decode_stripe_into(
|
|
metrics_path,
|
|
stage_metrics_enabled,
|
|
engine,
|
|
workspace,
|
|
state,
|
|
remaining_after_first,
|
|
&mut queued_buf,
|
|
);
|
|
record_get_stage_duration_if_enabled(metrics_path, GET_STAGE_DECODE, decode_stage_start);
|
|
match queued_result {
|
|
Ok(true) => {
|
|
remaining_after_first = remaining_after_first.saturating_sub(queued_buf.len());
|
|
queued_buffers.push_back(queued_buf);
|
|
}
|
|
Ok(false) => {
|
|
reusable_buffers.push(queued_buf);
|
|
if remaining_after_first > 0 {
|
|
deferred_error = Some(DiskError::LessData.into());
|
|
}
|
|
break;
|
|
}
|
|
Err(err) => {
|
|
reusable_buffers.push(queued_buf);
|
|
deferred_error = Some(err);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
record_get_stage_duration_if_enabled(metrics_path, GET_STAGE_FILL, fill_stage_start);
|
|
|
|
FillResult {
|
|
result,
|
|
queued_buffers,
|
|
reusable_buffers,
|
|
deferred_error,
|
|
}
|
|
}
|
|
|
|
impl<S, E> Drop for ErasureDecodeReader<S, E>
|
|
where
|
|
E: ErasureDecodeEngine,
|
|
{
|
|
fn drop(&mut self) {
|
|
if self.fill.take().is_some() {
|
|
rustfs_io_metrics::record_get_object_fill_cancelled_on_drop(self.metrics_path, self.fill_policy.as_str());
|
|
}
|
|
if let Some(worker) = self.worker.take() {
|
|
worker.task.abort();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<S, E> Unpin for ErasureDecodeReader<S, E> where E: ErasureDecodeEngine {}
|
|
|
|
impl<S, E> AsyncRead for ErasureDecodeReader<S, E>
|
|
where
|
|
S: ShardStripeSource + Send + 'static,
|
|
E: ErasureDecodeEngine + Clone + Send + Sync + 'static,
|
|
{
|
|
fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<io::Result<()>> {
|
|
let filled_before_poll = buf.filled().len();
|
|
|
|
loop {
|
|
if self.output_pos < self.output_buf.len() {
|
|
if self.prefetched_bufs.len() < self.fill_policy.max_inflight()
|
|
&& self.prefetch_error.is_none()
|
|
&& self.remaining > 0
|
|
&& let Poll::Ready(result) = self.poll_prefetch(cx)
|
|
{
|
|
result?;
|
|
}
|
|
|
|
let available = &self.output_buf[self.output_pos..];
|
|
let read_buf_remaining_before = buf.remaining();
|
|
let output_remaining_before = available.len();
|
|
let copy_len = available.len().min(buf.remaining());
|
|
let copy_start = get_stage_timer_if_enabled(self.stage_metrics_enabled);
|
|
buf.put_slice(&available[..copy_len]);
|
|
self.output_pos += copy_len;
|
|
if copy_len > 0
|
|
&& let Some(copy_start) = copy_start
|
|
{
|
|
rustfs_io_metrics::record_get_object_reader_copy(
|
|
self.metrics_path,
|
|
copy_len,
|
|
read_buf_remaining_before,
|
|
output_remaining_before,
|
|
copy_start.elapsed().as_secs_f64(),
|
|
);
|
|
}
|
|
if buf.remaining() == 0 {
|
|
return Poll::Ready(Ok(()));
|
|
}
|
|
continue;
|
|
}
|
|
|
|
self.recycle_drained_output_buf();
|
|
|
|
if let Some(next_buf) = self.prefetched_bufs.pop_front() {
|
|
rustfs_io_metrics::record_get_object_reader_buffer(self.metrics_path, GET_READER_BUFFER_OUTPUT, next_buf.len());
|
|
self.output_buf = next_buf;
|
|
self.output_pos = 0;
|
|
continue;
|
|
}
|
|
|
|
if self.prefetch_error.is_some() && buf.filled().len() > filled_before_poll {
|
|
return Poll::Ready(Ok(()));
|
|
}
|
|
if let Some(err) = self.prefetch_error.take() {
|
|
return Poll::Ready(Err(err));
|
|
}
|
|
|
|
if self.remaining == 0 {
|
|
return Poll::Ready(Ok(()));
|
|
}
|
|
|
|
if self.output_wait_started_at.is_none() {
|
|
self.output_wait_started_at = get_stage_timer_if_enabled(self.stage_metrics_enabled);
|
|
}
|
|
let prefetch = match self.poll_prefetch(cx) {
|
|
Poll::Ready(result) => result,
|
|
Poll::Pending if buf.filled().len() > filled_before_poll => return Poll::Ready(Ok(())),
|
|
Poll::Pending => return Poll::Pending,
|
|
};
|
|
if self.stage_metrics_enabled
|
|
&& let Some(started_at) = self.output_wait_started_at.take()
|
|
{
|
|
rustfs_io_metrics::record_get_object_fill_waited_by_output(
|
|
self.metrics_path,
|
|
self.fill_policy.as_str(),
|
|
started_at.elapsed().as_secs_f64(),
|
|
);
|
|
}
|
|
prefetch?;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) struct SyncErasureDecodeReader<R> {
|
|
inner: Mutex<R>,
|
|
metrics_path: &'static str,
|
|
stage_metrics_enabled: bool,
|
|
}
|
|
|
|
impl<R> SyncErasureDecodeReader<R> {
|
|
pub(crate) fn new(inner: R) -> Self {
|
|
Self::new_with_metrics_path(inner, GET_OBJECT_PATH_CODEC_STREAMING)
|
|
}
|
|
|
|
pub(crate) fn new_with_metrics_path(inner: R, metrics_path: &'static str) -> Self {
|
|
Self {
|
|
inner: Mutex::new(inner),
|
|
metrics_path,
|
|
stage_metrics_enabled: rustfs_io_metrics::get_stage_metrics_enabled(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<R> AsyncRead for SyncErasureDecodeReader<R>
|
|
where
|
|
R: AsyncRead + Unpin + Send,
|
|
{
|
|
fn poll_read(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<io::Result<()>> {
|
|
let stage_metrics_enabled = self.stage_metrics_enabled;
|
|
let lock_wait_start = get_stage_timer_if_enabled(stage_metrics_enabled);
|
|
let mut inner = match self.inner.lock() {
|
|
Ok(inner) => {
|
|
record_get_stage_duration_if_enabled(self.metrics_path, GET_STAGE_OUTPUT_LOCK_WAIT, lock_wait_start);
|
|
inner
|
|
}
|
|
Err(_) => {
|
|
record_get_stage_duration_if_enabled(self.metrics_path, GET_STAGE_OUTPUT_LOCK_WAIT, lock_wait_start);
|
|
return Poll::Ready(Err(io::Error::other("erasure decode reader lock poisoned")));
|
|
}
|
|
};
|
|
let read_buf_remaining_before = stage_metrics_enabled.then(|| buf.remaining());
|
|
let filled_before = stage_metrics_enabled.then(|| buf.filled().len());
|
|
let poll_start = get_stage_timer_if_enabled(stage_metrics_enabled);
|
|
let result = Pin::new(&mut *inner).poll_read(cx, buf);
|
|
if let (Some(read_buf_remaining_before), Some(filled_before), Some(poll_start)) =
|
|
(read_buf_remaining_before, filled_before, poll_start)
|
|
{
|
|
let poll_duration = poll_start.elapsed().as_secs_f64();
|
|
let filled_bytes = buf.filled().len().saturating_sub(filled_before);
|
|
let poll_outcome = match &result {
|
|
Poll::Ready(Ok(())) if filled_bytes > 0 => GET_READER_POLL_READY_DATA,
|
|
Poll::Ready(Ok(())) => GET_READER_POLL_READY_EMPTY,
|
|
Poll::Ready(Err(_)) => GET_READER_POLL_READY_ERROR,
|
|
Poll::Pending => GET_READER_POLL_PENDING,
|
|
};
|
|
rustfs_io_metrics::record_get_object_stage_duration(self.metrics_path, GET_STAGE_OUTPUT_POLL, poll_duration);
|
|
rustfs_io_metrics::record_get_object_reader_poll(
|
|
self.metrics_path,
|
|
poll_outcome,
|
|
read_buf_remaining_before,
|
|
filled_bytes,
|
|
poll_duration,
|
|
);
|
|
}
|
|
result
|
|
}
|
|
}
|
|
|
|
fn decode_stripe_into<E>(
|
|
metrics_path: &'static str,
|
|
stage_metrics_enabled: bool,
|
|
engine: &E,
|
|
workspace: &mut E::Workspace,
|
|
state: StripeReadState,
|
|
remaining: usize,
|
|
output: &mut Vec<u8>,
|
|
) -> io::Result<bool>
|
|
where
|
|
E: ErasureDecodeEngine,
|
|
{
|
|
output.clear();
|
|
if state.slots().is_empty() {
|
|
return Ok(false);
|
|
}
|
|
if !state.can_decode() {
|
|
return Err(DiskError::ErasureReadQuorum.into());
|
|
}
|
|
|
|
let reconstruct_stage_start = get_stage_timer_if_enabled(stage_metrics_enabled);
|
|
if state.data_shards_complete(engine.data_shards()) {
|
|
rustfs_io_metrics::record_get_object_reconstruct_outcome(
|
|
metrics_path,
|
|
engine.engine_name(),
|
|
GET_RECONSTRUCT_OUTCOME_SKIP_DATA_COMPLETE,
|
|
);
|
|
record_get_stage_duration_if_enabled(metrics_path, GET_STAGE_RECONSTRUCT, reconstruct_stage_start);
|
|
let emit_stage_start = get_stage_timer_if_enabled(stage_metrics_enabled);
|
|
emit_data_shards_into(&state, engine.data_shards(), engine.block_size(), remaining, output)?;
|
|
record_get_stage_duration_if_enabled(metrics_path, GET_STAGE_EMIT, emit_stage_start);
|
|
return Ok(true);
|
|
}
|
|
|
|
let (mut shards, _errs) = state.into_parts();
|
|
let reconstruct_outcome = match engine.reconstruct_into(&mut shards, workspace) {
|
|
Ok(outcome) => outcome,
|
|
Err(err) => {
|
|
record_get_stage_duration_if_enabled(metrics_path, GET_STAGE_RECONSTRUCT, reconstruct_stage_start);
|
|
return Err(err);
|
|
}
|
|
};
|
|
rustfs_io_metrics::record_get_object_reconstruct_outcome(metrics_path, engine.engine_name(), reconstruct_outcome);
|
|
record_get_stage_duration_if_enabled(metrics_path, GET_STAGE_RECONSTRUCT, reconstruct_stage_start);
|
|
|
|
if shards.len() < engine.data_shards() {
|
|
return Err(io::Error::new(
|
|
ErrorKind::UnexpectedEof,
|
|
"decoded stripe has fewer shards than data shard count",
|
|
));
|
|
}
|
|
|
|
let emit_stage_start = get_stage_timer_if_enabled(stage_metrics_enabled);
|
|
reserve_output_capacity(output, engine.block_size().min(remaining));
|
|
for shard in shards.iter().take(engine.data_shards()) {
|
|
if output.len() >= remaining {
|
|
break;
|
|
}
|
|
let Some(shard) = shard else {
|
|
return Err(io::Error::new(ErrorKind::UnexpectedEof, "decoded stripe is missing a data shard"));
|
|
};
|
|
let copy_len = shard.len().min(remaining - output.len());
|
|
output.extend_from_slice(&shard[..copy_len]);
|
|
}
|
|
record_get_stage_duration_if_enabled(metrics_path, GET_STAGE_EMIT, emit_stage_start);
|
|
|
|
Ok(true)
|
|
}
|
|
|
|
fn emit_data_shards(state: &StripeReadState, data_shards: usize, block_size: usize, remaining: usize) -> io::Result<Vec<u8>> {
|
|
let mut output = Vec::new();
|
|
emit_data_shards_into(state, data_shards, block_size, remaining, &mut output)?;
|
|
Ok(output)
|
|
}
|
|
|
|
fn reserve_output_capacity(output: &mut Vec<u8>, target_capacity: usize) {
|
|
if output.capacity() < target_capacity {
|
|
output.reserve(target_capacity - output.capacity());
|
|
}
|
|
}
|
|
|
|
fn emit_data_shards_into(
|
|
state: &StripeReadState,
|
|
data_shards: usize,
|
|
block_size: usize,
|
|
remaining: usize,
|
|
output: &mut Vec<u8>,
|
|
) -> io::Result<()> {
|
|
output.clear();
|
|
reserve_output_capacity(output, block_size.min(remaining));
|
|
for index in 0..data_shards {
|
|
if output.len() >= remaining {
|
|
break;
|
|
}
|
|
let Some(slot) = state.slot_by_index(index) else {
|
|
return Err(io::Error::new(ErrorKind::UnexpectedEof, "decoded stripe is missing a data shard"));
|
|
};
|
|
let Some(shard) = slot.data_bytes() else {
|
|
return Err(io::Error::new(ErrorKind::UnexpectedEof, "decoded stripe is missing a data shard"));
|
|
};
|
|
let copy_len = shard.len().min(remaining - output.len());
|
|
output.extend_from_slice(&shard[..copy_len]);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::erasure::codec::bridge::{
|
|
CodecStreamingDecodeEngine, ErasureDecodeEngine, LegacyEcDecodeEngine, RustfsCodecDecodeEngine,
|
|
};
|
|
use crate::erasure::coding::decode::ParallelReader;
|
|
use crate::erasure::coding::{BitrotReader, BitrotWriter, Erasure};
|
|
use crate::set_disk::shard_source::{ShardSlot, StripeReadState};
|
|
use rustfs_utils::HashAlgorithm;
|
|
use std::collections::VecDeque;
|
|
use std::future::pending;
|
|
use std::io::Cursor;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
use temp_env::with_var;
|
|
use tokio::io::AsyncReadExt;
|
|
use tokio::sync::Notify;
|
|
use tokio::task::yield_now;
|
|
use tokio::time::{Duration, timeout};
|
|
|
|
struct VecStripeSource {
|
|
stripes: VecDeque<StripeReadState>,
|
|
read_quorum: usize,
|
|
read_count: Option<Arc<AtomicUsize>>,
|
|
}
|
|
|
|
struct BlockingSource {
|
|
started: Arc<Notify>,
|
|
dropped: Arc<AtomicUsize>,
|
|
read_quorum: usize,
|
|
}
|
|
|
|
struct BlockingSourceDropGuard {
|
|
dropped: Arc<AtomicUsize>,
|
|
}
|
|
|
|
impl Drop for BlockingSourceDropGuard {
|
|
fn drop(&mut self) {
|
|
self.dropped.fetch_add(1, Ordering::SeqCst);
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl ShardStripeSource for VecStripeSource {
|
|
async fn read_next_stripe(&mut self) -> StripeReadState {
|
|
if let Some(read_count) = &self.read_count {
|
|
read_count.fetch_add(1, Ordering::SeqCst);
|
|
}
|
|
self.stripes
|
|
.pop_front()
|
|
.unwrap_or_else(|| StripeReadState::new(Vec::new(), self.read_quorum))
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl ShardStripeSource for BlockingSource {
|
|
async fn read_next_stripe(&mut self) -> StripeReadState {
|
|
let _guard = BlockingSourceDropGuard {
|
|
dropped: Arc::clone(&self.dropped),
|
|
};
|
|
self.started.notify_one();
|
|
pending::<()>().await;
|
|
StripeReadState::new(Vec::new(), self.read_quorum)
|
|
}
|
|
}
|
|
|
|
fn source_from_data(erasure: &Erasure, data: &[u8], missing_indexes: &[usize]) -> VecStripeSource {
|
|
let read_quorum = erasure.data_shards;
|
|
let stripes = data
|
|
.chunks(erasure.block_size)
|
|
.map(|chunk| {
|
|
let shards = erasure
|
|
.encode_data(chunk)
|
|
.expect("test stripe should encode")
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(index, shard)| {
|
|
if missing_indexes.contains(&index) {
|
|
None
|
|
} else {
|
|
Some(shard.to_vec())
|
|
}
|
|
})
|
|
.collect();
|
|
StripeReadState::from_parts(shards, Vec::new(), read_quorum)
|
|
})
|
|
.collect();
|
|
|
|
VecStripeSource {
|
|
stripes,
|
|
read_quorum,
|
|
read_count: None,
|
|
}
|
|
}
|
|
|
|
async fn decode_all_with_engine<E>(
|
|
erasure: &Erasure,
|
|
engine: E,
|
|
data: &[u8],
|
|
missing_indexes: &[usize],
|
|
) -> io::Result<Vec<u8>>
|
|
where
|
|
E: ErasureDecodeEngine + Clone + Send + Sync + 'static,
|
|
{
|
|
let source = source_from_data(erasure, data, missing_indexes);
|
|
let mut reader = ErasureDecodeReader::new_with_fill_policy(
|
|
source,
|
|
engine,
|
|
data.len(),
|
|
GET_OBJECT_PATH_CODEC_STREAMING,
|
|
FillPolicy::SingleInFlight,
|
|
)?;
|
|
let mut decoded = Vec::new();
|
|
reader.read_to_end(&mut decoded).await?;
|
|
Ok(decoded)
|
|
}
|
|
|
|
async fn decode_all(erasure: Erasure, data: &[u8], missing_indexes: &[usize]) -> io::Result<Vec<u8>> {
|
|
let engine = LegacyEcDecodeEngine::new(erasure.clone());
|
|
decode_all_with_engine(&erasure, engine, data, missing_indexes).await
|
|
}
|
|
|
|
async fn bitrot_readers_from_encoded(
|
|
erasure: &Erasure,
|
|
data: &[u8],
|
|
missing_indexes: &[usize],
|
|
corrupt_indexes: &[usize],
|
|
hash_algo: HashAlgorithm,
|
|
) -> Vec<Option<BitrotReader<Cursor<Vec<u8>>>>> {
|
|
let shard_size = erasure.shard_size();
|
|
let mut readers = Vec::with_capacity(erasure.data_shards + erasure.parity_shards);
|
|
for (index, shard) in erasure
|
|
.encode_data(data)
|
|
.expect("test stripe should encode")
|
|
.into_iter()
|
|
.enumerate()
|
|
{
|
|
if missing_indexes.contains(&index) {
|
|
readers.push(None);
|
|
continue;
|
|
}
|
|
|
|
let mut writer = BitrotWriter::new(Cursor::new(Vec::new()), shard_size, hash_algo.clone());
|
|
writer.write(&shard).await.expect("test shard should write with bitrot hash");
|
|
let mut encoded = writer.into_inner().into_inner();
|
|
if corrupt_indexes.contains(&index) {
|
|
let data_offset = hash_algo.size();
|
|
if let Some(byte) = encoded.get_mut(data_offset) {
|
|
*byte ^= 0x80;
|
|
}
|
|
}
|
|
readers.push(Some(BitrotReader::new(Cursor::new(encoded), shard_size, hash_algo.clone(), false)));
|
|
}
|
|
readers
|
|
}
|
|
|
|
#[test]
|
|
fn fill_policy_defaults_to_dual_inflight() {
|
|
with_var(ENV_RUSTFS_GET_CODEC_STREAMING_MAX_INFLIGHT, None::<&str>, || {
|
|
assert_eq!(FillPolicy::from_env(), FillPolicy::DualInFlight);
|
|
});
|
|
|
|
with_var(ENV_RUSTFS_GET_CODEC_STREAMING_MAX_INFLIGHT, Some("2"), || {
|
|
assert_eq!(FillPolicy::from_env(), FillPolicy::DualInFlight);
|
|
});
|
|
|
|
with_var(ENV_RUSTFS_GET_CODEC_STREAMING_MAX_INFLIGHT, Some("99"), || {
|
|
assert_eq!(FillPolicy::from_env(), FillPolicy::SingleInFlight);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
#[serial_test::serial]
|
|
fn erasure_decode_reader_caches_stage_metrics_enabled_at_construction() {
|
|
let erasure = Erasure::new(4, 2, 32);
|
|
let data = b"metrics switch cache";
|
|
|
|
rustfs_io_metrics::set_get_stage_metrics_enabled(false);
|
|
let source = source_from_data(&erasure, data, &[]);
|
|
let engine = LegacyEcDecodeEngine::new(erasure.clone());
|
|
let reader = ErasureDecodeReader::new_with_fill_policy(
|
|
source,
|
|
engine,
|
|
data.len(),
|
|
GET_OBJECT_PATH_CODEC_STREAMING,
|
|
FillPolicy::SingleInFlight,
|
|
)
|
|
.expect("reader should be constructed");
|
|
assert!(!reader.stage_metrics_enabled);
|
|
|
|
rustfs_io_metrics::set_get_stage_metrics_enabled(true);
|
|
let source = source_from_data(&erasure, data, &[]);
|
|
let engine = LegacyEcDecodeEngine::new(erasure);
|
|
let reader = ErasureDecodeReader::new_with_fill_policy(
|
|
source,
|
|
engine,
|
|
data.len(),
|
|
GET_OBJECT_PATH_CODEC_STREAMING,
|
|
FillPolicy::SingleInFlight,
|
|
)
|
|
.expect("reader should be constructed");
|
|
assert!(reader.stage_metrics_enabled);
|
|
|
|
rustfs_io_metrics::set_get_stage_metrics_enabled(false);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn erasure_decode_reader_reads_single_stripe() {
|
|
let erasure = Erasure::new(4, 2, 64);
|
|
let data = b"single stripe decode reader output";
|
|
|
|
let decoded = decode_all(erasure, data, &[])
|
|
.await
|
|
.expect("single stripe reader should decode");
|
|
|
|
assert_eq!(decoded, data);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn erasure_decode_reader_reads_multiple_stripes() {
|
|
let erasure = Erasure::new(4, 2, 32);
|
|
let data = (0..150u16).map(|value| value.to_le_bytes()[0]).collect::<Vec<_>>();
|
|
|
|
let decoded = decode_all(erasure, &data, &[])
|
|
.await
|
|
.expect("multi stripe reader should decode");
|
|
|
|
assert_eq!(decoded, data);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn erasure_decode_reader_stops_at_eof_for_empty_object() {
|
|
let erasure = Erasure::new(4, 2, 32);
|
|
let source = source_from_data(&erasure, &[], &[]);
|
|
let engine = LegacyEcDecodeEngine::new(erasure);
|
|
let mut reader = ErasureDecodeReader::new_with_fill_policy(
|
|
source,
|
|
engine,
|
|
0,
|
|
GET_OBJECT_PATH_CODEC_STREAMING,
|
|
FillPolicy::SingleInFlight,
|
|
)
|
|
.expect("empty reader should be constructed");
|
|
let mut decoded = Vec::new();
|
|
|
|
let read = reader
|
|
.read_to_end(&mut decoded)
|
|
.await
|
|
.expect("empty reader should finish without reading stripes");
|
|
|
|
assert_eq!(read, 0);
|
|
assert!(decoded.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn erasure_decode_reader_reconstructs_missing_data_shard() {
|
|
let erasure = Erasure::new(4, 2, 32);
|
|
let data = (0..120u16)
|
|
.map(|value| value.wrapping_mul(17).to_le_bytes()[0])
|
|
.collect::<Vec<_>>();
|
|
|
|
let decoded = decode_all(erasure, &data, &[1])
|
|
.await
|
|
.expect("reader should reconstruct one missing data shard");
|
|
|
|
assert_eq!(decoded, data);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn erasure_decode_reader_dual_inflight_prefetches_an_extra_stripe() {
|
|
let erasure = Erasure::new(4, 2, 16);
|
|
let data = (0..48u8).collect::<Vec<_>>();
|
|
let read_count = Arc::new(AtomicUsize::new(0));
|
|
let mut source = source_from_data(&erasure, &data, &[]);
|
|
source.read_count = Some(Arc::clone(&read_count));
|
|
let engine = LegacyEcDecodeEngine::new(erasure);
|
|
let mut reader = ErasureDecodeReader::new_with_fill_policy(
|
|
source,
|
|
engine,
|
|
data.len(),
|
|
GET_OBJECT_PATH_CODEC_STREAMING,
|
|
FillPolicy::DualInFlight,
|
|
)
|
|
.expect("reader should be constructed");
|
|
let mut first_read = [0u8; 1];
|
|
|
|
let read = reader.read(&mut first_read).await.expect("first read should succeed");
|
|
|
|
assert_eq!(read, 1);
|
|
timeout(Duration::from_secs(1), async {
|
|
while read_count.load(Ordering::SeqCst) < 3 {
|
|
yield_now().await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("dual inflight policy should prefetch two future stripes before current output drains");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn erasure_decode_reader_defers_short_read_error_until_buffer_drains() {
|
|
let erasure = Erasure::new(4, 2, 32);
|
|
let first_stripe = (0..32u8).collect::<Vec<_>>();
|
|
let source = VecStripeSource {
|
|
stripes: VecDeque::from([
|
|
source_from_data(&erasure, &first_stripe, &[])
|
|
.stripes
|
|
.pop_front()
|
|
.expect("first stripe should exist"),
|
|
StripeReadState::new(Vec::new(), erasure.data_shards),
|
|
]),
|
|
read_quorum: erasure.data_shards,
|
|
read_count: None,
|
|
};
|
|
let engine = LegacyEcDecodeEngine::new(erasure);
|
|
let mut reader = ErasureDecodeReader::new_with_fill_policy(
|
|
source,
|
|
engine,
|
|
first_stripe.len() + 1,
|
|
GET_OBJECT_PATH_CODEC_STREAMING,
|
|
FillPolicy::SingleInFlight,
|
|
)
|
|
.expect("reader should be constructed");
|
|
let mut decoded = Vec::new();
|
|
|
|
let err = reader
|
|
.read_to_end(&mut decoded)
|
|
.await
|
|
.expect_err("short-read error should surface after buffered output drains");
|
|
|
|
assert_eq!(err.kind(), ErrorKind::Other);
|
|
assert_eq!(decoded, first_stripe);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn erasure_decode_reader_drop_aborts_inflight_fill_task() {
|
|
let started = Arc::new(Notify::new());
|
|
let dropped = Arc::new(AtomicUsize::new(0));
|
|
let source = BlockingSource {
|
|
started: Arc::clone(&started),
|
|
dropped: Arc::clone(&dropped),
|
|
read_quorum: 1,
|
|
};
|
|
let engine = LegacyEcDecodeEngine::new(Erasure::new(1, 0, 32));
|
|
let task = tokio::spawn(async move {
|
|
let mut reader = ErasureDecodeReader::new_with_fill_policy(
|
|
source,
|
|
engine,
|
|
1,
|
|
GET_OBJECT_PATH_CODEC_STREAMING,
|
|
FillPolicy::SingleInFlight,
|
|
)
|
|
.expect("reader should be constructed");
|
|
let mut first_read = [0u8; 1];
|
|
let _ = reader.read(&mut first_read).await;
|
|
});
|
|
|
|
started.notified().await;
|
|
task.abort();
|
|
let _ = task.await;
|
|
|
|
timeout(Duration::from_secs(1), async {
|
|
while dropped.load(Ordering::SeqCst) == 0 {
|
|
yield_now().await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("reader drop should abort the in-flight fill task");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn erasure_decode_reader_rejects_inconsistent_reconstruction_sources() {
|
|
const DATA_SHARDS: usize = 2;
|
|
const PARITY_SHARDS: usize = 2;
|
|
const BLOCK_SIZE: usize = 64;
|
|
|
|
let erasure = Erasure::new(DATA_SHARDS, PARITY_SHARDS, BLOCK_SIZE);
|
|
let data = (0u8..64u8).collect::<Vec<_>>();
|
|
let encoded = erasure.encode_data(&data).expect("test stripe should encode");
|
|
let mut corrupt_parity = encoded[DATA_SHARDS].to_vec();
|
|
corrupt_parity[0] ^= 0x80;
|
|
|
|
let source = VecStripeSource {
|
|
stripes: VecDeque::from([StripeReadState::from_parts(
|
|
vec![
|
|
None,
|
|
Some(encoded[1].to_vec()),
|
|
Some(corrupt_parity),
|
|
Some(encoded[DATA_SHARDS + 1].to_vec()),
|
|
],
|
|
Vec::new(),
|
|
DATA_SHARDS,
|
|
)]),
|
|
read_quorum: DATA_SHARDS,
|
|
read_count: None,
|
|
};
|
|
let engine = LegacyEcDecodeEngine::new(erasure);
|
|
let mut reader = ErasureDecodeReader::new_with_fill_policy(
|
|
source,
|
|
engine,
|
|
data.len(),
|
|
GET_OBJECT_PATH_CODEC_STREAMING,
|
|
FillPolicy::SingleInFlight,
|
|
)
|
|
.expect("reader should be constructed");
|
|
let mut decoded = Vec::new();
|
|
|
|
let err = reader
|
|
.read_to_end(&mut decoded)
|
|
.await
|
|
.expect_err("streaming reader must reject inconsistent reconstruction sources");
|
|
|
|
assert_eq!(err.kind(), ErrorKind::InvalidData);
|
|
assert!(err.to_string().contains("inconsistent read source shards"));
|
|
assert!(decoded.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn erasure_decode_reader_rustfs_engine_matches_legacy_with_missing_data() {
|
|
let erasure = Erasure::new(4, 2, 32);
|
|
let data = b"rustfs codec reader output must match legacy reader output exactly";
|
|
let legacy = LegacyEcDecodeEngine::new(erasure.clone());
|
|
let rustfs = RustfsCodecDecodeEngine::new(&erasure).expect("engine should be created");
|
|
|
|
let legacy_decoded = decode_all_with_engine(&erasure, legacy, data, &[1])
|
|
.await
|
|
.expect("legacy reader should decode");
|
|
let rustfs_decoded = decode_all_with_engine(&erasure, rustfs, data, &[1])
|
|
.await
|
|
.expect("rustfs codec reader should decode");
|
|
|
|
assert_eq!(rustfs_decoded, legacy_decoded);
|
|
assert_eq!(rustfs_decoded, data);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn erasure_decode_reader_rustfs_engine_handles_empty_object() {
|
|
let erasure = Erasure::new(4, 2, 32);
|
|
let engine = RustfsCodecDecodeEngine::new(&erasure).expect("engine should be created");
|
|
|
|
let decoded = decode_all_with_engine(&erasure, engine, b"", &[])
|
|
.await
|
|
.expect("empty object should decode");
|
|
|
|
assert!(decoded.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn erasure_decode_reader_rustfs_engine_recovers_after_bitrot_source_mismatch() {
|
|
let erasure = Erasure::new(4, 2, 64);
|
|
let data = (0..64u16)
|
|
.map(|value| value.wrapping_mul(29).to_le_bytes()[0])
|
|
.collect::<Vec<_>>();
|
|
let readers = bitrot_readers_from_encoded(&erasure, &data, &[0], &[1], HashAlgorithm::HighwayHash256).await;
|
|
let source = ParallelReader::new_with_metrics_path_and_reconstruction_verification(
|
|
readers,
|
|
erasure.clone(),
|
|
0,
|
|
data.len(),
|
|
Some(GET_OBJECT_PATH_CODEC_STREAMING),
|
|
);
|
|
let engine = RustfsCodecDecodeEngine::new(&erasure).expect("engine should be created");
|
|
let mut reader = ErasureDecodeReader::new_with_fill_policy(
|
|
source,
|
|
engine,
|
|
data.len(),
|
|
GET_OBJECT_PATH_CODEC_STREAMING,
|
|
FillPolicy::SingleInFlight,
|
|
)
|
|
.expect("reader should be constructed");
|
|
let mut decoded = Vec::new();
|
|
|
|
reader
|
|
.read_to_end(&mut decoded)
|
|
.await
|
|
.expect("rustfs reader should reconstruct from clean shards after bitrot mismatch");
|
|
|
|
assert_eq!(decoded, data);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn erasure_decode_reader_verifying_parallel_source_rejects_inconsistent_reconstruction_sources() {
|
|
const DATA_SHARDS: usize = 2;
|
|
const PARITY_SHARDS: usize = 2;
|
|
const BLOCK_SIZE: usize = 64;
|
|
|
|
let erasure = Erasure::new(DATA_SHARDS, PARITY_SHARDS, BLOCK_SIZE);
|
|
let data = (0u8..64u8).collect::<Vec<_>>();
|
|
let shard_size = erasure.shard_size();
|
|
let encoded = erasure.encode_data(&data).expect("test stripe should encode");
|
|
let mut corrupt_parity = encoded[DATA_SHARDS].to_vec();
|
|
corrupt_parity[0] ^= 0x80;
|
|
let readers = vec![
|
|
None,
|
|
Some(BitrotReader::new(
|
|
Cursor::new(encoded[1].to_vec()),
|
|
shard_size,
|
|
HashAlgorithm::None,
|
|
false,
|
|
)),
|
|
Some(BitrotReader::new(Cursor::new(corrupt_parity), shard_size, HashAlgorithm::None, false)),
|
|
Some(BitrotReader::new(
|
|
Cursor::new(encoded[DATA_SHARDS + 1].to_vec()),
|
|
shard_size,
|
|
HashAlgorithm::None,
|
|
false,
|
|
)),
|
|
];
|
|
let source = ParallelReader::new_with_metrics_path_and_reconstruction_verification(
|
|
readers,
|
|
erasure.clone(),
|
|
0,
|
|
data.len(),
|
|
Some(GET_OBJECT_PATH_CODEC_STREAMING),
|
|
);
|
|
let engine = LegacyEcDecodeEngine::new(erasure);
|
|
let mut reader = ErasureDecodeReader::new_with_fill_policy(
|
|
source,
|
|
engine,
|
|
data.len(),
|
|
GET_OBJECT_PATH_CODEC_STREAMING,
|
|
FillPolicy::SingleInFlight,
|
|
)
|
|
.expect("reader should be constructed");
|
|
let mut decoded = Vec::new();
|
|
|
|
let err = reader
|
|
.read_to_end(&mut decoded)
|
|
.await
|
|
.expect_err("streaming reader must reject inconsistent reconstruction sources");
|
|
|
|
assert_eq!(err.kind(), ErrorKind::InvalidData);
|
|
assert!(err.to_string().contains("inconsistent read source shards"));
|
|
assert!(decoded.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn erasure_decode_reader_codec_streaming_engine_enum_matches_legacy() {
|
|
let erasure = Erasure::new(4, 2, 32);
|
|
let data = b"selected codec streaming engine preserves reader output";
|
|
let legacy = CodecStreamingDecodeEngine::legacy(erasure.clone());
|
|
let rustfs = CodecStreamingDecodeEngine::rustfs(&erasure).expect("engine should be created");
|
|
|
|
let legacy_decoded = decode_all_with_engine(&erasure, legacy, data, &[2])
|
|
.await
|
|
.expect("legacy enum reader should decode");
|
|
let rustfs_decoded = decode_all_with_engine(&erasure, rustfs, data, &[2])
|
|
.await
|
|
.expect("rustfs enum reader should decode");
|
|
|
|
assert_eq!(rustfs_decoded, legacy_decoded);
|
|
assert_eq!(rustfs_decoded, data);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn erasure_decode_reader_reads_when_only_parity_shards_are_missing() {
|
|
let erasure = Erasure::new(4, 2, 32);
|
|
let data = (0..120u16)
|
|
.map(|value| value.wrapping_mul(11).to_le_bytes()[0])
|
|
.collect::<Vec<_>>();
|
|
|
|
let decoded = decode_all(erasure, &data, &[4, 5])
|
|
.await
|
|
.expect("reader should emit complete data shards without parity reconstruction");
|
|
|
|
assert_eq!(decoded, data);
|
|
}
|
|
|
|
#[test]
|
|
fn emit_data_shards_preserves_output_order_for_out_of_order_slots() {
|
|
let state = StripeReadState::new(
|
|
vec![
|
|
ShardSlot::data(1, b"cd".to_vec()),
|
|
ShardSlot::data(0, b"ab".to_vec()),
|
|
ShardSlot::data(2, b"ef".to_vec()),
|
|
],
|
|
2,
|
|
);
|
|
|
|
let output = emit_data_shards(&state, 3, 6, 5).expect("out-of-order data slots should emit by shard index");
|
|
|
|
assert_eq!(output, b"abcde");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn erasure_decode_reader_reports_short_source() {
|
|
let erasure = Erasure::new(4, 2, 32);
|
|
let source = VecStripeSource {
|
|
stripes: VecDeque::new(),
|
|
read_quorum: erasure.data_shards,
|
|
read_count: None,
|
|
};
|
|
let engine = LegacyEcDecodeEngine::new(erasure);
|
|
let mut reader = ErasureDecodeReader::new_with_fill_policy(
|
|
source,
|
|
engine,
|
|
1,
|
|
GET_OBJECT_PATH_CODEC_STREAMING,
|
|
FillPolicy::SingleInFlight,
|
|
)
|
|
.expect("reader should be constructed");
|
|
let mut decoded = Vec::new();
|
|
|
|
let err = reader
|
|
.read_to_end(&mut decoded)
|
|
.await
|
|
.expect_err("reader should reject EOF before requested length");
|
|
|
|
assert_eq!(err.kind(), ErrorKind::Other);
|
|
assert!(decoded.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn erasure_decode_reader_prefetches_next_stripe_while_output_remains() {
|
|
let erasure = Erasure::new(4, 2, 32);
|
|
let data = (0..96u16)
|
|
.map(|value| value.wrapping_mul(3).to_le_bytes()[0])
|
|
.collect::<Vec<_>>();
|
|
let read_count = Arc::new(AtomicUsize::new(0));
|
|
let mut source = source_from_data(&erasure, &data, &[]);
|
|
source.read_count = Some(Arc::clone(&read_count));
|
|
let engine = LegacyEcDecodeEngine::new(erasure);
|
|
let mut reader = ErasureDecodeReader::new_with_fill_policy(
|
|
source,
|
|
engine,
|
|
data.len(),
|
|
GET_OBJECT_PATH_CODEC_STREAMING,
|
|
FillPolicy::SingleInFlight,
|
|
)
|
|
.expect("reader should be constructed");
|
|
let mut first_read = [0u8; 1];
|
|
|
|
let read = reader.read(&mut first_read).await.expect("first read should succeed");
|
|
|
|
assert_eq!(read, first_read.len());
|
|
assert_eq!(first_read[0], data[0]);
|
|
timeout(Duration::from_secs(1), async {
|
|
while read_count.load(Ordering::SeqCst) < 2 {
|
|
yield_now().await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("reader should start reading the next stripe before the current output buffer is fully consumed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn erasure_decode_reader_reuses_output_buffers_after_drain() {
|
|
let erasure = Erasure::new(4, 2, 32);
|
|
let data = (0..128u16)
|
|
.map(|value| value.wrapping_mul(5).to_le_bytes()[0])
|
|
.collect::<Vec<_>>();
|
|
let source = source_from_data(&erasure, &data, &[]);
|
|
let engine = LegacyEcDecodeEngine::new(erasure.clone());
|
|
let mut reader = ErasureDecodeReader::new_with_fill_policy(
|
|
source,
|
|
engine,
|
|
data.len(),
|
|
GET_OBJECT_PATH_CODEC_STREAMING,
|
|
FillPolicy::SingleInFlight,
|
|
)
|
|
.expect("reader should be constructed");
|
|
let mut decoded = Vec::new();
|
|
|
|
reader
|
|
.read_to_end(&mut decoded)
|
|
.await
|
|
.expect("reader should decode all stripes");
|
|
|
|
assert_eq!(decoded, data);
|
|
assert!(reader.reusable_output_bufs.len() <= reader.max_reusable_output_bufs());
|
|
assert!(
|
|
reader
|
|
.reusable_output_bufs
|
|
.iter()
|
|
.any(|buf| buf.capacity() >= erasure.block_size),
|
|
"drained stripe output buffers should be available for reuse"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn erasure_decode_reader_reuses_single_fill_worker_across_fills() {
|
|
let erasure = Erasure::new(4, 2, 32);
|
|
let data = (0..128u16)
|
|
.map(|value| value.wrapping_mul(7).to_le_bytes()[0])
|
|
.collect::<Vec<_>>();
|
|
let source = source_from_data(&erasure, &data, &[]);
|
|
let engine = LegacyEcDecodeEngine::new(erasure);
|
|
let mut reader = ErasureDecodeReader::new_with_fill_policy(
|
|
source,
|
|
engine,
|
|
data.len(),
|
|
GET_OBJECT_PATH_CODEC_STREAMING,
|
|
FillPolicy::SingleInFlight,
|
|
)
|
|
.expect("reader should be constructed");
|
|
let mut decoded = Vec::new();
|
|
let mut first_read = [0u8; 1];
|
|
|
|
let read = reader.read(&mut first_read).await.expect("first read should succeed");
|
|
decoded.extend_from_slice(&first_read[..read]);
|
|
|
|
assert!(reader.worker.is_some());
|
|
assert!(reader.source.is_none());
|
|
assert!(reader.engine.is_none());
|
|
assert!(reader.workspace.is_none());
|
|
|
|
reader
|
|
.read_to_end(&mut decoded)
|
|
.await
|
|
.expect("reader should continue using the fill worker");
|
|
|
|
assert_eq!(decoded, data);
|
|
assert!(reader.worker.is_some());
|
|
}
|
|
}
|