mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-28 16:07:05 +00:00
perf(ecstore): guard inline data-read metadata early-stop (#6069)
Add a default-off inline-only data-read metadata early-stop gate that verifies inline plaintext before cancelling pending metadata tasks. Keep non-inline, prepared, and request-shape-sensitive reads on full fanout, and record scheduled/completed/cancelled ReadVersion lifecycle metrics for normal fanout completion. Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -880,12 +880,44 @@ mod prepared_get_object_metadata_tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::ecstore_validation_blackbox::make_local_set_disks;
|
use crate::ecstore_validation_blackbox::make_local_set_disks;
|
||||||
use crate::object_api::{BLOCK_SIZE_V2, PutObjReader};
|
use crate::object_api::{BLOCK_SIZE_V2, PutObjReader};
|
||||||
use crate::set_disk::core::io_primitives::disk_call_counters;
|
use crate::set_disk::core::io_primitives::{bounded_metadata_fanout_order, disk_call_counters, rename_fanout_barrier};
|
||||||
use crate::storage_api_contracts::bucket::{BucketOperations as _, MakeBucketOptions};
|
use crate::storage_api_contracts::bucket::{BucketOperations as _, MakeBucketOptions};
|
||||||
use crate::storage_api_contracts::object::{ObjectIO as _, ObjectOperations as _};
|
use crate::storage_api_contracts::object::{ObjectIO as _, ObjectOperations as _};
|
||||||
|
use crate::test_metrics::CapturingRecorder;
|
||||||
use http::HeaderMap;
|
use http::HeaderMap;
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
|
const READ_VERSION_BARRIER_GUARD: std::time::Duration = std::time::Duration::from_secs(10);
|
||||||
|
|
||||||
|
fn object_with_initial_data_shards(bucket: &str, prefix: &str) -> String {
|
||||||
|
(0..1000)
|
||||||
|
.map(|index| format!("{prefix}-{index}.bin"))
|
||||||
|
.find(|name| {
|
||||||
|
let order = bounded_metadata_fanout_order(bucket, name, 4, 2);
|
||||||
|
let distribution = FileInfo::new(&[bucket, name].join("/"), 2, 2).erasure.distribution;
|
||||||
|
let mut seen = [false; 2];
|
||||||
|
for disk_index in order.into_iter().take(3) {
|
||||||
|
if let Some(block_index @ 1..=2) = distribution.get(disk_index).copied() {
|
||||||
|
seen[block_index - 1] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seen.into_iter().all(|seen| seen)
|
||||||
|
})
|
||||||
|
.expect("test should find an object whose initial fanout covers both data shards")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bounded_spare_disk_index(bucket: &str, object: &str) -> usize {
|
||||||
|
*bounded_metadata_fanout_order(bucket, object, 4, 2)
|
||||||
|
.get(3)
|
||||||
|
.expect("4-disk test geometry should leave one bounded spare disk")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bounded_slow_initial_disk_index(bucket: &str, object: &str) -> usize {
|
||||||
|
*bounded_metadata_fanout_order(bucket, object, 4, 2)
|
||||||
|
.get(2)
|
||||||
|
.expect("4-disk test geometry should include a third initial metadata disk")
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn prepared_metadata_is_consumed_exactly_once() {
|
async fn prepared_metadata_is_consumed_exactly_once() {
|
||||||
let snapshot = GetObjectFileInfo::owned(FileInfo::default(), Vec::new(), Vec::new());
|
let snapshot = GetObjectFileInfo::owned(FileInfo::default(), Vec::new(), Vec::new());
|
||||||
@@ -1002,6 +1034,307 @@ mod prepared_get_object_metadata_tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial_test::serial(body_cache_hook)]
|
||||||
|
fn inline_data_read_early_stop_reader_returns_exact_body() {
|
||||||
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.expect("current-thread runtime should build");
|
||||||
|
let bucket = "inline-data-read-early-stop-reader";
|
||||||
|
let object = object_with_initial_data_shards(bucket, "inline-data-read-early-stop-reader-object");
|
||||||
|
let payload = b"inline early-stop reader payload".repeat(256);
|
||||||
|
let recorder = CapturingRecorder::default();
|
||||||
|
let previous_gate = rustfs_io_metrics::get_stage_metrics_enabled();
|
||||||
|
rustfs_io_metrics::set_get_stage_metrics_enabled(true);
|
||||||
|
|
||||||
|
let (restored, object_size, calls_total) = metrics::with_local_recorder(&recorder, || {
|
||||||
|
runtime.block_on(async {
|
||||||
|
let (_dirs, set_disks) = make_local_set_disks(4, 2).await;
|
||||||
|
let opts = ObjectOptions {
|
||||||
|
no_lock: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
set_disks
|
||||||
|
.make_bucket(bucket, &MakeBucketOptions::default())
|
||||||
|
.await
|
||||||
|
.expect("bucket should be created");
|
||||||
|
let mut put_reader = PutObjReader::from_vec(payload.clone());
|
||||||
|
set_disks
|
||||||
|
.put_object(bucket, &object, &mut put_reader, &opts)
|
||||||
|
.await
|
||||||
|
.expect("inline object should be written");
|
||||||
|
|
||||||
|
temp_env::async_with_vars(
|
||||||
|
[
|
||||||
|
("RUSTFS_GET_METADATA_EARLY_STOP_ENABLE", Some("true")),
|
||||||
|
("RUSTFS_GET_METADATA_DATA_READ_EARLY_STOP_ENABLE", Some("true")),
|
||||||
|
("RUSTFS_GET_METADATA_EARLY_STOP_BOUNDED_FANOUT", Some("true")),
|
||||||
|
],
|
||||||
|
async {
|
||||||
|
let slow_initial_disk = bounded_slow_initial_disk_index(bucket, &object);
|
||||||
|
let barrier =
|
||||||
|
rename_fanout_barrier::arm(&object, slow_initial_disk, rename_fanout_barrier::PHASE_READ_VERSION);
|
||||||
|
let calls = disk_call_counters::observe(&object);
|
||||||
|
let set_disks_for_read = Arc::clone(&set_disks);
|
||||||
|
let opts_for_read = opts.clone();
|
||||||
|
let object_for_read = object.clone();
|
||||||
|
let mut open_reader = tokio::spawn(async move {
|
||||||
|
set_disks_for_read
|
||||||
|
.get_object_reader(bucket, &object_for_read, None, HeaderMap::new(), &opts_for_read)
|
||||||
|
.await
|
||||||
|
});
|
||||||
|
|
||||||
|
tokio::time::timeout(READ_VERSION_BARRIER_GUARD, barrier.wait_until_paused())
|
||||||
|
.await
|
||||||
|
.expect("bounded inline GET should pause a slow initial metadata read");
|
||||||
|
let mut reader = tokio::time::timeout(READ_VERSION_BARRIER_GUARD, &mut open_reader)
|
||||||
|
.await
|
||||||
|
.expect("production inline GET should return before the paused metadata response")
|
||||||
|
.expect("inline GET reader task should not panic")
|
||||||
|
.expect("inline GET reader should open");
|
||||||
|
let object_size = reader.object_info.size;
|
||||||
|
let mut restored = Vec::new();
|
||||||
|
reader
|
||||||
|
.stream
|
||||||
|
.read_to_end(&mut restored)
|
||||||
|
.await
|
||||||
|
.expect("inline GET body should stream");
|
||||||
|
|
||||||
|
(restored, object_size, calls.total(disk_call_counters::KIND_READ_VERSION))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
});
|
||||||
|
rustfs_io_metrics::set_get_stage_metrics_enabled(previous_gate);
|
||||||
|
|
||||||
|
assert_eq!(object_size, payload.len() as i64);
|
||||||
|
assert_eq!(restored, payload);
|
||||||
|
assert_eq!(calls_total, 4, "bounded production GET should schedule the initial quorum plus one spare");
|
||||||
|
assert_eq!(
|
||||||
|
recorder.histogram_values(
|
||||||
|
"rustfs_io_get_object_metadata_fanout_scheduled",
|
||||||
|
&[("path", GET_OBJECT_PATH_LEGACY_DUPLEX)]
|
||||||
|
),
|
||||||
|
vec![4.0],
|
||||||
|
"bounded production GET should record all scheduled metadata tasks"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
recorder.histogram_values(
|
||||||
|
"rustfs_io_get_object_metadata_fanout_completed",
|
||||||
|
&[("path", GET_OBJECT_PATH_LEGACY_DUPLEX)]
|
||||||
|
),
|
||||||
|
vec![3.0],
|
||||||
|
"bounded production GET should record only observed metadata responses as completed"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
recorder.histogram_values(
|
||||||
|
"rustfs_io_get_object_metadata_fanout_cancelled",
|
||||||
|
&[("path", GET_OBJECT_PATH_LEGACY_DUPLEX)]
|
||||||
|
),
|
||||||
|
vec![1.0],
|
||||||
|
"bounded production GET should record the aborted slow metadata task"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial_test::serial(body_cache_hook)]
|
||||||
|
fn prepared_metadata_uses_full_fanout_even_when_data_read_early_stop_is_enabled() {
|
||||||
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.expect("current-thread runtime should build");
|
||||||
|
let bucket = "prepared-metadata-early-stop-enabled";
|
||||||
|
let object = object_with_initial_data_shards(bucket, "prepared-metadata-early-stop-enabled-object");
|
||||||
|
let payload = b"prepared metadata early-stop enabled payload".repeat(16);
|
||||||
|
let recorder = CapturingRecorder::default();
|
||||||
|
let previous_gate = rustfs_io_metrics::get_stage_metrics_enabled();
|
||||||
|
rustfs_io_metrics::set_get_stage_metrics_enabled(true);
|
||||||
|
|
||||||
|
let (restored, calls_total) = metrics::with_local_recorder(&recorder, || {
|
||||||
|
runtime.block_on(async {
|
||||||
|
let (_dirs, set_disks) = make_local_set_disks(4, 2).await;
|
||||||
|
let opts = ObjectOptions {
|
||||||
|
no_lock: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
set_disks
|
||||||
|
.make_bucket(bucket, &MakeBucketOptions::default())
|
||||||
|
.await
|
||||||
|
.expect("bucket should be created");
|
||||||
|
let mut put_reader = PutObjReader::from_vec(payload.clone());
|
||||||
|
set_disks
|
||||||
|
.put_object(bucket, &object, &mut put_reader, &opts)
|
||||||
|
.await
|
||||||
|
.expect("object should be written");
|
||||||
|
|
||||||
|
temp_env::async_with_vars(
|
||||||
|
[
|
||||||
|
("RUSTFS_GET_METADATA_EARLY_STOP_ENABLE", Some("true")),
|
||||||
|
("RUSTFS_GET_METADATA_DATA_READ_EARLY_STOP_ENABLE", Some("true")),
|
||||||
|
("RUSTFS_GET_METADATA_EARLY_STOP_BOUNDED_FANOUT", Some("true")),
|
||||||
|
],
|
||||||
|
async {
|
||||||
|
let calls = disk_call_counters::observe(&object);
|
||||||
|
let metadata = set_disks
|
||||||
|
.prepare_get_object_metadata(bucket, &object, &opts)
|
||||||
|
.await
|
||||||
|
.expect("prepared metadata should resolve");
|
||||||
|
let calls_total = calls.total(disk_call_counters::KIND_READ_VERSION);
|
||||||
|
|
||||||
|
let mut reader = set_disks
|
||||||
|
.get_object_reader_with_prepared_metadata(bucket, &object, None, HeaderMap::new(), &opts, metadata)
|
||||||
|
.await
|
||||||
|
.expect("prepared body reader should open");
|
||||||
|
let mut restored = Vec::new();
|
||||||
|
reader
|
||||||
|
.stream
|
||||||
|
.read_to_end(&mut restored)
|
||||||
|
.await
|
||||||
|
.expect("prepared body should stream");
|
||||||
|
(restored, calls_total)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
});
|
||||||
|
rustfs_io_metrics::set_get_stage_metrics_enabled(previous_gate);
|
||||||
|
|
||||||
|
assert_eq!(restored, payload);
|
||||||
|
assert_eq!(
|
||||||
|
calls_total, 4,
|
||||||
|
"prepared metadata must opt out of data-read early-stop until the read shape is known"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
recorder.histogram_values(
|
||||||
|
"rustfs_io_get_object_metadata_fanout_scheduled",
|
||||||
|
&[("path", GET_OBJECT_PATH_LEGACY_DUPLEX)]
|
||||||
|
),
|
||||||
|
vec![4.0],
|
||||||
|
"prepared metadata should schedule the full metadata fanout"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
recorder.histogram_values(
|
||||||
|
"rustfs_io_get_object_metadata_fanout_completed",
|
||||||
|
&[("path", GET_OBJECT_PATH_LEGACY_DUPLEX)]
|
||||||
|
),
|
||||||
|
vec![4.0],
|
||||||
|
"prepared metadata must wait for every scheduled metadata response"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
recorder.histogram_values(
|
||||||
|
"rustfs_io_get_object_metadata_fanout_cancelled",
|
||||||
|
&[("path", GET_OBJECT_PATH_LEGACY_DUPLEX)]
|
||||||
|
),
|
||||||
|
vec![0.0],
|
||||||
|
"prepared metadata must not cancel metadata responses"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial_test::serial(body_cache_hook)]
|
||||||
|
fn data_read_early_stop_request_shapes_full_wait_in_production_reader() {
|
||||||
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.expect("current-thread runtime should build");
|
||||||
|
let bucket = "data-read-early-stop-shape-reader";
|
||||||
|
let payload = b"shape-gated inline reader payload".repeat(256);
|
||||||
|
|
||||||
|
for (object_prefix, range, configure_opts, expected_body) in [
|
||||||
|
(
|
||||||
|
"data-read-early-stop-range-reader-object",
|
||||||
|
Some(HTTPRangeSpec {
|
||||||
|
start: 0,
|
||||||
|
end: 3,
|
||||||
|
is_suffix_length: false,
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
payload[..4].to_vec(),
|
||||||
|
),
|
||||||
|
("data-read-early-stop-part-reader-object", None, Some(1), payload.clone()),
|
||||||
|
] {
|
||||||
|
let recorder = CapturingRecorder::default();
|
||||||
|
let previous_gate = rustfs_io_metrics::get_stage_metrics_enabled();
|
||||||
|
rustfs_io_metrics::set_get_stage_metrics_enabled(true);
|
||||||
|
let (restored, calls_total) = metrics::with_local_recorder(&recorder, || {
|
||||||
|
runtime.block_on(async {
|
||||||
|
let (_dirs, set_disks) = make_local_set_disks(4, 2).await;
|
||||||
|
let object = object_with_initial_data_shards(bucket, object_prefix);
|
||||||
|
let mut opts = ObjectOptions {
|
||||||
|
no_lock: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
opts.part_number = configure_opts;
|
||||||
|
|
||||||
|
set_disks
|
||||||
|
.make_bucket(bucket, &MakeBucketOptions::default())
|
||||||
|
.await
|
||||||
|
.expect("bucket should be created");
|
||||||
|
let mut put_reader = PutObjReader::from_vec(payload.clone());
|
||||||
|
set_disks
|
||||||
|
.put_object(bucket, &object, &mut put_reader, &opts)
|
||||||
|
.await
|
||||||
|
.expect("inline object should be written");
|
||||||
|
|
||||||
|
temp_env::async_with_vars(
|
||||||
|
[
|
||||||
|
("RUSTFS_GET_METADATA_EARLY_STOP_ENABLE", Some("true")),
|
||||||
|
("RUSTFS_GET_METADATA_DATA_READ_EARLY_STOP_ENABLE", Some("true")),
|
||||||
|
("RUSTFS_GET_METADATA_EARLY_STOP_BOUNDED_FANOUT", Some("true")),
|
||||||
|
],
|
||||||
|
async {
|
||||||
|
let calls = disk_call_counters::observe(&object);
|
||||||
|
let mut reader = set_disks
|
||||||
|
.get_object_reader(bucket, &object, range, HeaderMap::new(), &opts)
|
||||||
|
.await
|
||||||
|
.expect("shape-gated GET reader should open");
|
||||||
|
let mut restored = Vec::new();
|
||||||
|
reader
|
||||||
|
.stream
|
||||||
|
.read_to_end(&mut restored)
|
||||||
|
.await
|
||||||
|
.expect("shape-gated GET body should stream");
|
||||||
|
(restored, calls.total(disk_call_counters::KIND_READ_VERSION))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
});
|
||||||
|
rustfs_io_metrics::set_get_stage_metrics_enabled(previous_gate);
|
||||||
|
|
||||||
|
assert_eq!(restored, expected_body);
|
||||||
|
assert_eq!(calls_total, 4, "shape-gated production GET should keep full metadata fanout");
|
||||||
|
assert_eq!(
|
||||||
|
recorder.histogram_values(
|
||||||
|
"rustfs_io_get_object_metadata_fanout_scheduled",
|
||||||
|
&[("path", GET_OBJECT_PATH_LEGACY_DUPLEX)]
|
||||||
|
),
|
||||||
|
vec![4.0],
|
||||||
|
"shape-gated production GET should schedule the full metadata fanout"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
recorder.histogram_values(
|
||||||
|
"rustfs_io_get_object_metadata_fanout_completed",
|
||||||
|
&[("path", GET_OBJECT_PATH_LEGACY_DUPLEX)]
|
||||||
|
),
|
||||||
|
vec![4.0],
|
||||||
|
"shape-gated production GET must wait for every scheduled metadata response"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
recorder.histogram_values(
|
||||||
|
"rustfs_io_get_object_metadata_fanout_cancelled",
|
||||||
|
&[("path", GET_OBJECT_PATH_LEGACY_DUPLEX)]
|
||||||
|
),
|
||||||
|
vec![0.0],
|
||||||
|
"shape-gated production GET must not cancel metadata responses"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial_test::serial(body_cache_hook)]
|
#[serial_test::serial(body_cache_hook)]
|
||||||
async fn prepared_reader_rebuilds_object_info_when_precomputed_value_is_absent() {
|
async fn prepared_reader_rebuilds_object_info_when_precomputed_value_is_absent() {
|
||||||
@@ -1105,7 +1438,7 @@ impl SetDisks {
|
|||||||
object: &str,
|
object: &str,
|
||||||
opts: &ObjectOptions,
|
opts: &ObjectOptions,
|
||||||
) -> Result<PreparedGetObjectMetadata> {
|
) -> Result<PreparedGetObjectMetadata> {
|
||||||
let snapshot = self.get_object_fileinfo(bucket, object, opts, true, true).await?;
|
let snapshot = self.get_object_fileinfo(bucket, object, opts, true, false).await?;
|
||||||
let object_info = build_get_object_info(snapshot.fi(), bucket, object, opts.versioned || opts.version_suspended);
|
let object_info = build_get_object_info(snapshot.fi(), bucket, object, opts.versioned || opts.version_suspended);
|
||||||
Ok(PreparedGetObjectMetadata {
|
Ok(PreparedGetObjectMetadata {
|
||||||
snapshot,
|
snapshot,
|
||||||
@@ -3411,9 +3744,15 @@ fn collect_inline_data_shard_fileinfos_by_index<'a>(
|
|||||||
if block_index == 0 || block_index > data_shards {
|
if block_index == 0 || block_index > data_shards {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if file_info.erasure.index != block_index {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if !file_info.has_valid_erasure_geometry() {
|
if !file_info.has_valid_erasure_geometry() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if !core::io_primitives::metadata_early_stop_candidate_matches(file_info, fi) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if file_info.data.as_ref().is_none_or(|data| data.is_empty()) {
|
if file_info.data.as_ref().is_none_or(|data| data.is_empty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -9221,6 +9560,9 @@ mod tests {
|
|||||||
HashAlgorithm::HighwayHash256S
|
HashAlgorithm::HighwayHash256S
|
||||||
};
|
};
|
||||||
let shards = erasure.encode_data(payload).expect("payload should encode");
|
let shards = erasure.encode_data(payload).expect("payload should encode");
|
||||||
|
let version_id = Some(Uuid::new_v4());
|
||||||
|
let data_dir = Some(Uuid::new_v4());
|
||||||
|
let mod_time = Some(OffsetDateTime::now_utc());
|
||||||
let mut files = Vec::with_capacity(shards.len());
|
let mut files = Vec::with_capacity(shards.len());
|
||||||
|
|
||||||
for shard in shards {
|
for shard in shards {
|
||||||
@@ -9233,6 +9575,16 @@ mod tests {
|
|||||||
writer.shutdown().await.expect("inline writer should shutdown");
|
writer.shutdown().await.expect("inline writer should shutdown");
|
||||||
let data = writer.into_inline_data().expect("inline data should be retained");
|
let data = writer.into_inline_data().expect("inline data should be retained");
|
||||||
let mut file = FileInfo::new("bucket/object", erasure.data_shards, erasure.parity_shards);
|
let mut file = FileInfo::new("bucket/object", erasure.data_shards, erasure.parity_shards);
|
||||||
|
file.volume = "bucket".to_string();
|
||||||
|
file.name = "object".to_string();
|
||||||
|
file.size = i64::try_from(payload.len()).expect("test payload should fit i64");
|
||||||
|
file.is_latest = true;
|
||||||
|
file.version_id = version_id;
|
||||||
|
file.data_dir = data_dir;
|
||||||
|
file.mod_time = mod_time;
|
||||||
|
file.metadata.insert("etag".to_string(), "etag-inline".to_string());
|
||||||
|
file.add_object_part(1, "part-etag-inline".to_string(), payload.len(), file.mod_time, file.size, None, None);
|
||||||
|
file.set_inline_data();
|
||||||
file.erasure.index = files.len() + 1;
|
file.erasure.index = files.len() + 1;
|
||||||
file.data = Some(Bytes::from(data));
|
file.data = Some(Bytes::from(data));
|
||||||
files.push(file);
|
files.push(file);
|
||||||
@@ -9245,16 +9597,40 @@ mod tests {
|
|||||||
inline_bitrot_files_for_payload_with_mode(payload, false).await
|
inline_bitrot_files_for_payload_with_mode(payload, false).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn disk_ordered_fileinfos(files: &[FileInfo]) -> Vec<FileInfo> {
|
||||||
|
let distribution = &files
|
||||||
|
.first()
|
||||||
|
.expect("inline data shard fixture should include metadata")
|
||||||
|
.erasure
|
||||||
|
.distribution;
|
||||||
|
distribution
|
||||||
|
.iter()
|
||||||
|
.map(|block_index| {
|
||||||
|
files
|
||||||
|
.get(block_index.checked_sub(1).expect("erasure block indexes are one-based"))
|
||||||
|
.expect("inline data shard fixture should include every distributed shard")
|
||||||
|
.clone()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn inline_data_shard_fileinfo(
|
fn inline_data_shard_fileinfo(
|
||||||
name: &str,
|
|
||||||
data_blocks: usize,
|
data_blocks: usize,
|
||||||
parity_blocks: usize,
|
parity_blocks: usize,
|
||||||
erasure_index: usize,
|
erasure_index: usize,
|
||||||
distribution: &[usize],
|
distribution: &[usize],
|
||||||
data: Option<&'static [u8]>,
|
data: Option<&'static [u8]>,
|
||||||
) -> FileInfo {
|
) -> FileInfo {
|
||||||
let mut fi = FileInfo::new(name, data_blocks, parity_blocks);
|
let mut fi = FileInfo::new("object", data_blocks, parity_blocks);
|
||||||
fi.name = name.to_string();
|
fi.name = "object".to_string();
|
||||||
|
fi.volume = "bucket".to_string();
|
||||||
|
fi.size = 4;
|
||||||
|
fi.is_latest = true;
|
||||||
|
fi.data_dir = Some(Uuid::nil());
|
||||||
|
fi.mod_time = Some(OffsetDateTime::UNIX_EPOCH);
|
||||||
|
fi.metadata.insert("etag".to_string(), "etag-inline".to_string());
|
||||||
|
fi.add_object_part(1, "part-etag-inline".to_string(), 4, fi.mod_time, 4, None, None);
|
||||||
|
fi.set_inline_data();
|
||||||
fi.erasure.index = erasure_index;
|
fi.erasure.index = erasure_index;
|
||||||
fi.erasure.distribution = distribution.to_vec();
|
fi.erasure.distribution = distribution.to_vec();
|
||||||
fi.data = data.map(Bytes::from_static);
|
fi.data = data.map(Bytes::from_static);
|
||||||
@@ -9264,36 +9640,41 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn collect_inline_data_shards_by_index_uses_distribution_order() {
|
fn collect_inline_data_shards_by_index_uses_distribution_order() {
|
||||||
let distribution = vec![3, 1, 5, 2, 4, 6];
|
let distribution = vec![3, 1, 5, 2, 4, 6];
|
||||||
let mut fi = FileInfo::new("object", 4, 2);
|
let mut fi = inline_data_shard_fileinfo(4, 2, 1, &distribution, Some(b"x"));
|
||||||
|
fi.erasure.index = 1;
|
||||||
fi.erasure.distribution = distribution.clone();
|
fi.erasure.distribution = distribution.clone();
|
||||||
let files = vec![
|
let files = vec![
|
||||||
inline_data_shard_fileinfo("block-3", 4, 2, 3, &distribution, Some(b"c")),
|
inline_data_shard_fileinfo(4, 2, 3, &distribution, Some(b"c")),
|
||||||
inline_data_shard_fileinfo("block-1", 4, 2, 1, &distribution, Some(b"a")),
|
inline_data_shard_fileinfo(4, 2, 1, &distribution, Some(b"a")),
|
||||||
inline_data_shard_fileinfo("parity-5", 4, 2, 5, &distribution, Some(b"p")),
|
inline_data_shard_fileinfo(4, 2, 5, &distribution, Some(b"p")),
|
||||||
inline_data_shard_fileinfo("block-2", 4, 2, 2, &distribution, Some(b"b")),
|
inline_data_shard_fileinfo(4, 2, 2, &distribution, Some(b"b")),
|
||||||
inline_data_shard_fileinfo("block-4", 4, 2, 4, &distribution, Some(b"d")),
|
inline_data_shard_fileinfo(4, 2, 4, &distribution, Some(b"d")),
|
||||||
inline_data_shard_fileinfo("parity-6", 4, 2, 6, &distribution, Some(b"q")),
|
inline_data_shard_fileinfo(4, 2, 6, &distribution, Some(b"q")),
|
||||||
];
|
];
|
||||||
|
|
||||||
let data_files =
|
let data_files =
|
||||||
collect_inline_data_shard_fileinfos_by_index(&files, &fi, 4, |_| true).expect("all data shards should be collected");
|
collect_inline_data_shard_fileinfos_by_index(&files, &fi, 4, |_| true).expect("all data shards should be collected");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
data_files.iter().map(|file| file.name.as_str()).collect::<Vec<_>>(),
|
data_files
|
||||||
["block-1", "block-2", "block-3", "block-4"]
|
.iter()
|
||||||
|
.map(|file| file.data.as_deref().expect("fixture carries inline bytes"))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
[b"a".as_slice(), b"b".as_slice(), b"c".as_slice(), b"d".as_slice()]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn collect_inline_data_shards_by_index_rejects_missing_data_shard() {
|
fn collect_inline_data_shards_by_index_rejects_missing_data_shard() {
|
||||||
let distribution = vec![1, 2, 3, 4];
|
let distribution = vec![1, 2, 3, 4];
|
||||||
let mut fi = FileInfo::new("object", 2, 2);
|
let mut fi = inline_data_shard_fileinfo(2, 2, 1, &distribution, Some(b"x"));
|
||||||
|
fi.erasure.index = 1;
|
||||||
fi.erasure.distribution = distribution.clone();
|
fi.erasure.distribution = distribution.clone();
|
||||||
let files = vec![
|
let files = vec![
|
||||||
inline_data_shard_fileinfo("block-1", 2, 2, 1, &distribution, Some(b"a")),
|
inline_data_shard_fileinfo(2, 2, 1, &distribution, Some(b"a")),
|
||||||
inline_data_shard_fileinfo("block-2", 2, 2, 2, &distribution, None),
|
inline_data_shard_fileinfo(2, 2, 2, &distribution, None),
|
||||||
inline_data_shard_fileinfo("parity-3", 2, 2, 3, &distribution, Some(b"p")),
|
inline_data_shard_fileinfo(2, 2, 3, &distribution, Some(b"p")),
|
||||||
inline_data_shard_fileinfo("parity-4", 2, 2, 4, &distribution, Some(b"q")),
|
inline_data_shard_fileinfo(2, 2, 4, &distribution, Some(b"q")),
|
||||||
];
|
];
|
||||||
|
|
||||||
assert!(collect_inline_data_shard_fileinfos_by_index(&files, &fi, 2, |_| true).is_none());
|
assert!(collect_inline_data_shard_fileinfos_by_index(&files, &fi, 2, |_| true).is_none());
|
||||||
@@ -9426,10 +9807,8 @@ mod tests {
|
|||||||
|
|
||||||
let payload = vec![b'i'; 192 * 1024];
|
let payload = vec![b'i'; 192 * 1024];
|
||||||
let (erasure, files, _read_length, _checksum_algo) = inline_bitrot_files_for_payload(&payload).await;
|
let (erasure, files, _read_length, _checksum_algo) = inline_bitrot_files_for_payload(&payload).await;
|
||||||
let mut fi = FileInfo::new("bucket/object", erasure.data_shards, erasure.parity_shards);
|
let fi = files[0].clone();
|
||||||
fi.size = payload.len() as i64;
|
let disk_files = disk_ordered_fileinfos(&files);
|
||||||
fi.data = files[0].data.clone();
|
|
||||||
fi.add_object_part(1, String::new(), payload.len(), None, payload.len() as i64, None, None);
|
|
||||||
|
|
||||||
let disks = vec![Some(disk); erasure.total_shard_count()];
|
let disks = vec![Some(disk); erasure.total_shard_count()];
|
||||||
let metrics_size_bucket = rustfs_io_metrics::get_object_size_bucket(fi.size);
|
let metrics_size_bucket = rustfs_io_metrics::get_object_size_bucket(fi.size);
|
||||||
@@ -9438,7 +9817,7 @@ mod tests {
|
|||||||
"bucket",
|
"bucket",
|
||||||
"object",
|
"object",
|
||||||
&fi,
|
&fi,
|
||||||
&files,
|
&disk_files,
|
||||||
&disks,
|
&disks,
|
||||||
true,
|
true,
|
||||||
GET_CODEC_STREAMING_OBJECT_CLASS_PLAIN_SINGLE_PART,
|
GET_CODEC_STREAMING_OBJECT_CLASS_PLAIN_SINGLE_PART,
|
||||||
@@ -9469,10 +9848,8 @@ mod tests {
|
|||||||
let payload = vec![b'v'; 64 * 1024];
|
let payload = vec![b'v'; 64 * 1024];
|
||||||
let payload_size = i64::try_from(payload.len()).expect("test payload size should fit i64");
|
let payload_size = i64::try_from(payload.len()).expect("test payload size should fit i64");
|
||||||
let (erasure, files, _read_length, _checksum_algo) = inline_bitrot_files_for_payload(&payload).await;
|
let (erasure, files, _read_length, _checksum_algo) = inline_bitrot_files_for_payload(&payload).await;
|
||||||
let mut fi = FileInfo::new("bucket/object", erasure.data_shards, erasure.parity_shards);
|
let fi = files[0].clone();
|
||||||
fi.size = payload_size;
|
let disk_files = disk_ordered_fileinfos(&files);
|
||||||
fi.data = files[0].data.clone();
|
|
||||||
fi.add_object_part(1, String::new(), payload.len(), None, payload_size, None, None);
|
|
||||||
|
|
||||||
let mut object_info = ObjectInfo {
|
let mut object_info = ObjectInfo {
|
||||||
size: payload_size,
|
size: payload_size,
|
||||||
@@ -9503,7 +9880,7 @@ mod tests {
|
|||||||
"bucket",
|
"bucket",
|
||||||
"object",
|
"object",
|
||||||
&fi,
|
&fi,
|
||||||
&files,
|
&disk_files,
|
||||||
&vec![Some(disk); erasure.total_shard_count()],
|
&vec![Some(disk); erasure.total_shard_count()],
|
||||||
true,
|
true,
|
||||||
GET_CODEC_STREAMING_OBJECT_CLASS_PLAIN_SINGLE_PART,
|
GET_CODEC_STREAMING_OBJECT_CLASS_PLAIN_SINGLE_PART,
|
||||||
|
|||||||
@@ -188,6 +188,74 @@ async fn get_object_reader_with_context(
|
|||||||
GetObjectReader::new_with_resolver(reader, range, object_info, opts, headers, ctx.object_encryption_resolver()).await
|
GetObjectReader::new_with_resolver(reader, range, object_info, opts, headers, ctx.object_encryption_resolver()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn data_read_metadata_early_stop_request_shape_allowed(range: &Option<HTTPRangeSpec>, opts: &ObjectOptions) -> bool {
|
||||||
|
range.is_none()
|
||||||
|
&& opts.part_number.is_none()
|
||||||
|
&& opts.version_id.is_none()
|
||||||
|
&& !opts.incl_free_versions
|
||||||
|
&& !opts.skip_free_version
|
||||||
|
&& !opts.raw_data_movement_read
|
||||||
|
&& !opts.data_movement
|
||||||
|
&& !crate::object_api::restore_request_active(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod data_read_metadata_early_stop_request_shape_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn data_read_metadata_early_stop_only_allows_whole_latest_plain_get_shape() {
|
||||||
|
assert!(data_read_metadata_early_stop_request_shape_allowed(&None, &ObjectOptions::default()));
|
||||||
|
|
||||||
|
let range = Some(HTTPRangeSpec {
|
||||||
|
is_suffix_length: false,
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
});
|
||||||
|
assert!(!data_read_metadata_early_stop_request_shape_allowed(&range, &ObjectOptions::default()));
|
||||||
|
|
||||||
|
let part_opts = ObjectOptions {
|
||||||
|
part_number: Some(1),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert!(!data_read_metadata_early_stop_request_shape_allowed(&None, &part_opts));
|
||||||
|
|
||||||
|
let version_opts = ObjectOptions {
|
||||||
|
version_id: Some(Uuid::new_v4().to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert!(!data_read_metadata_early_stop_request_shape_allowed(&None, &version_opts));
|
||||||
|
|
||||||
|
let incl_free_opts = ObjectOptions {
|
||||||
|
incl_free_versions: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert!(!data_read_metadata_early_stop_request_shape_allowed(&None, &incl_free_opts));
|
||||||
|
|
||||||
|
let skip_free_opts = ObjectOptions {
|
||||||
|
skip_free_version: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert!(!data_read_metadata_early_stop_request_shape_allowed(&None, &skip_free_opts));
|
||||||
|
|
||||||
|
let data_movement_opts = ObjectOptions {
|
||||||
|
data_movement: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert!(!data_read_metadata_early_stop_request_shape_allowed(&None, &data_movement_opts));
|
||||||
|
|
||||||
|
let raw_data_movement_opts = ObjectOptions {
|
||||||
|
raw_data_movement_read: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert!(!data_read_metadata_early_stop_request_shape_allowed(&None, &raw_data_movement_opts));
|
||||||
|
|
||||||
|
let mut restore_opts = ObjectOptions::default();
|
||||||
|
restore_opts.transition.restore_request.days = Some(1);
|
||||||
|
assert!(!data_read_metadata_early_stop_request_shape_allowed(&None, &restore_opts));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Length of the full plaintext body when — and only when — this read's output
|
/// Length of the full plaintext body when — and only when — this read's output
|
||||||
/// is exactly the object's complete plaintext, so the app-layer body cache may
|
/// is exactly the object's complete plaintext, so the app-layer body cache may
|
||||||
/// serve it in place of the erasure read.
|
/// serve it in place of the erasure read.
|
||||||
@@ -431,7 +499,16 @@ impl crate::storage_api_contracts::object::ObjectIO for SetDisks {
|
|||||||
let (snapshot, prepared_object_info) = if let Some(prepared) = take_prepared_get_object_metadata() {
|
let (snapshot, prepared_object_info) = if let Some(prepared) = take_prepared_get_object_metadata() {
|
||||||
(prepared.snapshot, prepared.object_info)
|
(prepared.snapshot, prepared.object_info)
|
||||||
} else {
|
} else {
|
||||||
match self.get_object_fileinfo(bucket, object, opts, true, true).await {
|
match self
|
||||||
|
.get_object_fileinfo(
|
||||||
|
bucket,
|
||||||
|
object,
|
||||||
|
opts,
|
||||||
|
true,
|
||||||
|
data_read_metadata_early_stop_request_shape_allowed(&range, opts),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(snapshot) => (snapshot, None),
|
Ok(snapshot) => (snapshot, None),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
rustfs_io_metrics::record_get_object_metadata_phase_duration(metadata_stage_start.elapsed().as_secs_f64());
|
rustfs_io_metrics::record_get_object_metadata_phase_duration(metadata_stage_start.elapsed().as_secs_f64());
|
||||||
@@ -6102,7 +6179,10 @@ mod inline_put_commit_path_tests {
|
|||||||
mod get_object_downstream_close_accounting_tests {
|
mod get_object_downstream_close_accounting_tests {
|
||||||
use super::hermetic_set_disks_support::hermetic_set_disks;
|
use super::hermetic_set_disks_support::hermetic_set_disks;
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::diagnostics::get::{GET_OBJECT_PATH_INTERNAL_META, GET_STAGE_DECODE, GET_STAGE_EMIT, GetObjectFailureReason};
|
use crate::diagnostics::get::{
|
||||||
|
GET_METADATA_EARLY_STOP_REASON_UNSAFE_REQUEST, GET_OBJECT_PATH_INTERNAL_META, GET_STAGE_DECODE, GET_STAGE_EMIT,
|
||||||
|
GetObjectFailureReason,
|
||||||
|
};
|
||||||
use crate::disk::RUSTFS_META_BUCKET;
|
use crate::disk::RUSTFS_META_BUCKET;
|
||||||
use crate::storage_api_contracts::bucket::{BucketOperations as _, MakeBucketOptions};
|
use crate::storage_api_contracts::bucket::{BucketOperations as _, MakeBucketOptions};
|
||||||
use crate::storage_api_contracts::object::{ObjectIO as _, ObjectOperations as _};
|
use crate::storage_api_contracts::object::{ObjectIO as _, ObjectOperations as _};
|
||||||
@@ -6221,7 +6301,22 @@ mod get_object_downstream_close_accounting_tests {
|
|||||||
let previous_gate = rustfs_io_metrics::get_stage_metrics_enabled();
|
let previous_gate = rustfs_io_metrics::get_stage_metrics_enabled();
|
||||||
rustfs_io_metrics::set_get_stage_metrics_enabled(true);
|
rustfs_io_metrics::set_get_stage_metrics_enabled(true);
|
||||||
|
|
||||||
let (internal_missing, legacy_unknown, internal_fanout, legacy_fanout) = metrics::with_local_recorder(&recorder, || {
|
let (
|
||||||
|
internal_missing,
|
||||||
|
legacy_unknown,
|
||||||
|
internal_fanout,
|
||||||
|
legacy_fanout,
|
||||||
|
internal_scheduled,
|
||||||
|
legacy_scheduled,
|
||||||
|
internal_completed,
|
||||||
|
legacy_completed,
|
||||||
|
internal_cancelled,
|
||||||
|
legacy_cancelled,
|
||||||
|
internal_unsafe_miss,
|
||||||
|
legacy_unsafe_miss,
|
||||||
|
internal_saved,
|
||||||
|
legacy_saved,
|
||||||
|
) = metrics::with_local_recorder(&recorder, || {
|
||||||
runtime.block_on(async {
|
runtime.block_on(async {
|
||||||
let (_temp_dirs, _disk_stores, set_disks) = hermetic_set_disks(4).await;
|
let (_temp_dirs, _disk_stores, set_disks) = hermetic_set_disks(4).await;
|
||||||
let options = ObjectOptions {
|
let options = ObjectOptions {
|
||||||
@@ -6265,6 +6360,54 @@ mod get_object_downstream_close_accounting_tests {
|
|||||||
"rustfs_io_get_object_metadata_fanout_error_responses",
|
"rustfs_io_get_object_metadata_fanout_error_responses",
|
||||||
&[("path", GET_OBJECT_PATH_LEGACY_DUPLEX)],
|
&[("path", GET_OBJECT_PATH_LEGACY_DUPLEX)],
|
||||||
),
|
),
|
||||||
|
recorder.histogram_values(
|
||||||
|
"rustfs_io_get_object_metadata_fanout_scheduled",
|
||||||
|
&[("path", GET_OBJECT_PATH_INTERNAL_META)],
|
||||||
|
),
|
||||||
|
recorder.histogram_values(
|
||||||
|
"rustfs_io_get_object_metadata_fanout_scheduled",
|
||||||
|
&[("path", GET_OBJECT_PATH_LEGACY_DUPLEX)],
|
||||||
|
),
|
||||||
|
recorder.histogram_values(
|
||||||
|
"rustfs_io_get_object_metadata_fanout_completed",
|
||||||
|
&[("path", GET_OBJECT_PATH_INTERNAL_META)],
|
||||||
|
),
|
||||||
|
recorder.histogram_values(
|
||||||
|
"rustfs_io_get_object_metadata_fanout_completed",
|
||||||
|
&[("path", GET_OBJECT_PATH_LEGACY_DUPLEX)],
|
||||||
|
),
|
||||||
|
recorder.histogram_values(
|
||||||
|
"rustfs_io_get_object_metadata_fanout_cancelled",
|
||||||
|
&[("path", GET_OBJECT_PATH_INTERNAL_META)],
|
||||||
|
),
|
||||||
|
recorder.histogram_values(
|
||||||
|
"rustfs_io_get_object_metadata_fanout_cancelled",
|
||||||
|
&[("path", GET_OBJECT_PATH_LEGACY_DUPLEX)],
|
||||||
|
),
|
||||||
|
recorder.counter_value(
|
||||||
|
"rustfs_io_get_object_metadata_early_stop_total",
|
||||||
|
&[
|
||||||
|
("path", GET_OBJECT_PATH_INTERNAL_META),
|
||||||
|
("decision", "miss"),
|
||||||
|
("reason", GET_METADATA_EARLY_STOP_REASON_UNSAFE_REQUEST),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
recorder.counter_value(
|
||||||
|
"rustfs_io_get_object_metadata_early_stop_total",
|
||||||
|
&[
|
||||||
|
("path", GET_OBJECT_PATH_LEGACY_DUPLEX),
|
||||||
|
("decision", "miss"),
|
||||||
|
("reason", GET_METADATA_EARLY_STOP_REASON_UNSAFE_REQUEST),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
recorder.histogram_values(
|
||||||
|
"rustfs_io_get_object_metadata_early_stop_saved_responses",
|
||||||
|
&[("path", GET_OBJECT_PATH_INTERNAL_META)],
|
||||||
|
),
|
||||||
|
recorder.histogram_values(
|
||||||
|
"rustfs_io_get_object_metadata_early_stop_saved_responses",
|
||||||
|
&[("path", GET_OBJECT_PATH_LEGACY_DUPLEX)],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -6277,6 +6420,50 @@ mod get_object_downstream_close_accounting_tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(internal_fanout, vec![4.0], "internal metadata fanout must retain its path label");
|
assert_eq!(internal_fanout, vec![4.0], "internal metadata fanout must retain its path label");
|
||||||
assert!(legacy_fanout.is_empty(), "internal metadata fanout must not leak into legacy_duplex");
|
assert!(legacy_fanout.is_empty(), "internal metadata fanout must not leak into legacy_duplex");
|
||||||
|
assert_eq!(
|
||||||
|
internal_scheduled,
|
||||||
|
vec![4.0],
|
||||||
|
"internal metadata lifecycle scheduled count must retain its path label"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
legacy_scheduled.is_empty(),
|
||||||
|
"internal metadata lifecycle scheduled count must not leak into legacy_duplex"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
internal_completed,
|
||||||
|
vec![4.0],
|
||||||
|
"internal metadata lifecycle completed count must retain its path label"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
legacy_completed.is_empty(),
|
||||||
|
"internal metadata lifecycle completed count must not leak into legacy_duplex"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
internal_cancelled,
|
||||||
|
vec![0.0],
|
||||||
|
"internal metadata full-wait lifecycle must record zero cancellations"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
legacy_cancelled.is_empty(),
|
||||||
|
"internal metadata lifecycle cancelled count must not leak into legacy_duplex"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
internal_unsafe_miss, 1,
|
||||||
|
"internal metadata unsafe early-stop miss must retain its path label"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
legacy_unsafe_miss, 0,
|
||||||
|
"internal metadata unsafe early-stop miss must not leak into legacy_duplex"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
internal_saved,
|
||||||
|
vec![0.0],
|
||||||
|
"internal metadata unsafe miss must record zero saved responses on internal_meta"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
legacy_saved.is_empty(),
|
||||||
|
"internal metadata unsafe miss saved responses must not leak into legacy_duplex"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
|
|
||||||
//! test endpoint index settings
|
//! test endpoint index settings
|
||||||
|
|
||||||
|
#![recursion_limit = "256"]
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
//! bucket-metadata-sys OnceCell) — under `cargo nextest` each test runs
|
//! bucket-metadata-sys OnceCell) — under `cargo nextest` each test runs
|
||||||
//! in its own process so the OnceCell never collides.
|
//! in its own process so the OnceCell never collides.
|
||||||
|
|
||||||
|
#![recursion_limit = "256"]
|
||||||
|
|
||||||
use http::HeaderMap;
|
use http::HeaderMap;
|
||||||
use rustfs_common::heal_channel::{HealOpts, HealScanMode};
|
use rustfs_common::heal_channel::{HealOpts, HealScanMode};
|
||||||
use rustfs_heal::heal::{
|
use rustfs_heal::heal::{
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
//! These drive the REAL `ECStoreHealStorage` + `ECStore` against real disks.
|
//! These drive the REAL `ECStoreHealStorage` + `ECStore` against real disks.
|
||||||
//! Every test is `#[serial]`; under `cargo nextest` each runs in its own process.
|
//! Every test is `#[serial]`; under `cargo nextest` each runs in its own process.
|
||||||
|
|
||||||
|
#![recursion_limit = "256"]
|
||||||
|
|
||||||
use http::HeaderMap;
|
use http::HeaderMap;
|
||||||
use rustfs_common::heal_channel::{HealOpts, HealScanMode};
|
use rustfs_common::heal_channel::{HealOpts, HealScanMode};
|
||||||
use rustfs_heal::heal::storage::{
|
use rustfs_heal::heal::storage::{
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
#![recursion_limit = "256"]
|
||||||
|
|
||||||
use http::HeaderMap;
|
use http::HeaderMap;
|
||||||
use rustfs_common::heal_channel::{HealOpts, HealScanMode};
|
use rustfs_common::heal_channel::{HealOpts, HealScanMode};
|
||||||
use rustfs_heal::heal::{
|
use rustfs_heal::heal::{
|
||||||
|
|||||||
@@ -812,6 +812,17 @@ pub fn record_get_object_metadata_fanout_shape(path: &'static str, total: usize,
|
|||||||
.record(metadata_fanout_count_to_f64(non_valid));
|
.record(metadata_fanout_count_to_f64(non_valid));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Record task lifecycle shape for one GetObject metadata fanout.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn record_get_object_metadata_fanout_lifecycle(path: &'static str, scheduled: usize, completed: usize, cancelled: usize) {
|
||||||
|
if !get_stage_metrics_enabled() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
histogram!("rustfs_io_get_object_metadata_fanout_scheduled", "path" => path).record(metadata_fanout_count_to_f64(scheduled));
|
||||||
|
histogram!("rustfs_io_get_object_metadata_fanout_completed", "path" => path).record(metadata_fanout_count_to_f64(completed));
|
||||||
|
histogram!("rustfs_io_get_object_metadata_fanout_cancelled", "path" => path).record(metadata_fanout_count_to_f64(cancelled));
|
||||||
|
}
|
||||||
|
|
||||||
/// Record a guarded metadata early-stop hit for GetObject.
|
/// Record a guarded metadata early-stop hit for GetObject.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn record_get_object_metadata_early_stop_hit(path: &'static str, reason: &'static str) {
|
pub fn record_get_object_metadata_early_stop_hit(path: &'static str, reason: &'static str) {
|
||||||
@@ -2698,6 +2709,7 @@ mod tests {
|
|||||||
record_get_object_quorum_reached_latency("legacy_duplex", 0.002);
|
record_get_object_quorum_reached_latency("legacy_duplex", 0.002);
|
||||||
record_get_object_metadata_response("legacy_duplex", "valid");
|
record_get_object_metadata_response("legacy_duplex", "valid");
|
||||||
record_get_object_metadata_fanout_shape("legacy_duplex", 4, 3, 1, 1);
|
record_get_object_metadata_fanout_shape("legacy_duplex", 4, 3, 1, 1);
|
||||||
|
record_get_object_metadata_fanout_lifecycle("legacy_duplex", 4, 3, 1);
|
||||||
record_get_object_metadata_early_stop_hit("legacy_duplex", "valid_quorum");
|
record_get_object_metadata_early_stop_hit("legacy_duplex", "valid_quorum");
|
||||||
record_get_object_metadata_early_stop_miss("legacy_duplex", "insufficient_quorum");
|
record_get_object_metadata_early_stop_miss("legacy_duplex", "insufficient_quorum");
|
||||||
record_get_object_metadata_early_stop_saved_responses("legacy_duplex", 1);
|
record_get_object_metadata_early_stop_saved_responses("legacy_duplex", 1);
|
||||||
@@ -2768,6 +2780,38 @@ mod tests {
|
|||||||
assert!(remote_scheduled >= remote_avoid_potential);
|
assert!(remote_scheduled >= remote_avoid_potential);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn metadata_fanout_lifecycle_records_named_histograms() {
|
||||||
|
let _guard = METRICS_FLAG_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
let recorder = DebuggingRecorder::new();
|
||||||
|
let snapshotter = recorder.snapshotter();
|
||||||
|
|
||||||
|
metrics::with_local_recorder(&recorder, || {
|
||||||
|
set_get_stage_metrics_enabled(true);
|
||||||
|
record_get_object_metadata_fanout_lifecycle("legacy_duplex", 4, 3, 1);
|
||||||
|
set_get_stage_metrics_enabled(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
let metrics = snapshotter.snapshot().into_vec();
|
||||||
|
for (name, expected) in [
|
||||||
|
("rustfs_io_get_object_metadata_fanout_scheduled", 4.0),
|
||||||
|
("rustfs_io_get_object_metadata_fanout_completed", 3.0),
|
||||||
|
("rustfs_io_get_object_metadata_fanout_cancelled", 1.0),
|
||||||
|
] {
|
||||||
|
let value = metrics.iter().find_map(|(composite, _, _, value)| {
|
||||||
|
let has_path = composite
|
||||||
|
.key()
|
||||||
|
.labels()
|
||||||
|
.any(|label| label.key() == "path" && label.value() == "legacy_duplex");
|
||||||
|
(composite.kind() == MetricKind::Histogram && composite.key().name() == name && has_path).then_some(value)
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
matches!(value, Some(DebugValue::Histogram(values)) if values.len() == 1 && values[0].0 == expected),
|
||||||
|
"{name} must record the exact fanout lifecycle sample"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_record_get_object_fill_metrics() {
|
fn test_record_get_object_fill_metrics() {
|
||||||
record_get_object_fill_queued("codec_streaming", "single_inflight", 1);
|
record_get_object_fill_queued("codec_streaming", "single_inflight", 1);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
//! two are tested together because a reload is the only way to tell a real
|
//! two are tested together because a reload is the only way to tell a real
|
||||||
//! merge from one that happened to look right in the cache.
|
//! merge from one that happened to look right in the cache.
|
||||||
|
|
||||||
|
#![recursion_limit = "256"]
|
||||||
#![cfg(feature = "swift")]
|
#![cfg(feature = "swift")]
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
#![recursion_limit = "256"]
|
||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
#![warn(
|
#![warn(
|
||||||
// missing_docs,
|
// missing_docs,
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
#![recursion_limit = "256"]
|
||||||
|
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use rustfs_config::ENV_TEST_FORCE_IMMEDIATE_TRANSITION_ENQUEUE_TIMEOUT;
|
use rustfs_config::ENV_TEST_FORCE_IMMEDIATE_TRANSITION_ENQUEUE_TIMEOUT;
|
||||||
use rustfs_scanner::scanner_folder::ScannerItem;
|
use rustfs_scanner::scanner_folder::ScannerItem;
|
||||||
|
|||||||
Reference in New Issue
Block a user