mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-08 21:25:59 +00:00
Merge branch 'main' into cxymds/feat/pool-write-block-diagnostics
Signed-off-by: houseme <housemecn@gmail.com>
This commit is contained in:
@@ -66,6 +66,11 @@ Current guidance:
|
||||
|
||||
- `RUSTFS_BROWSER_REDIRECT_URL` sets the externally reachable browser origin used for OIDC callback, console success redirect, and logout fallback URLs. Configure it to the public scheme and authority without a path, for example `https://console.example.com`. In load-balancer deployments, keep OIDC authorize and callback requests on the same backend node because the in-flight OIDC `state` is local to the RustFS node.
|
||||
|
||||
## S3 API environment variables
|
||||
|
||||
- `RUSTFS_API_OBJECT_MAX_VERSIONS` caps the number of retained versions for a single object. It defaults to `9223372036854775807`, matching MinIO's practical-unlimited default. Set a positive integer to enforce a lower per-object metadata bound.
|
||||
- `MINIO_API_OBJECT_MAX_VERSIONS` is accepted as a compatibility alias when the canonical RustFS variable is not set.
|
||||
|
||||
## Distributed endpoint locality
|
||||
|
||||
- `RUSTFS_LOCAL_ENDPOINT_HOST` identifies this server's host in a distributed `RUSTFS_VOLUMES` topology without resolving every peer during startup. Set it to exactly one host, without a scheme, port, or path. It is accepted only for orchestrated URL topologies and must match at least one endpoint on the RustFS server port; invalid or unmatched values fail startup. Leave it unset to retain DNS-based locality discovery.
|
||||
|
||||
@@ -90,3 +90,15 @@ pub const ENV_API_MAX_CONNECTIONS: &str = "RUSTFS_API_MAX_CONNECTIONS";
|
||||
|
||||
/// Default for `RUSTFS_API_MAX_CONNECTIONS` (`0` = unlimited).
|
||||
pub const DEFAULT_API_MAX_CONNECTIONS: usize = 0;
|
||||
|
||||
/// Maximum retained versions per object.
|
||||
///
|
||||
/// The default follows MinIO and is effectively unlimited for practical
|
||||
/// deployments. Operators can lower it to bound per-object metadata growth.
|
||||
/// Environment variable: RUSTFS_API_OBJECT_MAX_VERSIONS
|
||||
/// MinIO-compatible alias: MINIO_API_OBJECT_MAX_VERSIONS
|
||||
/// Example: RUSTFS_API_OBJECT_MAX_VERSIONS=50000
|
||||
pub const ENV_API_OBJECT_MAX_VERSIONS: &str = "RUSTFS_API_OBJECT_MAX_VERSIONS";
|
||||
|
||||
/// Default for `RUSTFS_API_OBJECT_MAX_VERSIONS`.
|
||||
pub const DEFAULT_API_OBJECT_MAX_VERSIONS: u64 = 9_223_372_036_854_775_807;
|
||||
|
||||
@@ -425,6 +425,7 @@ impl From<rustfs_filemeta::Error> for DiskError {
|
||||
rustfs_filemeta::Error::FileVersionNotFound => DiskError::FileVersionNotFound,
|
||||
rustfs_filemeta::Error::FileCorrupt => DiskError::FileCorrupt,
|
||||
rustfs_filemeta::Error::MethodNotAllowed => DiskError::MethodNotAllowed,
|
||||
rustfs_filemeta::Error::MaxVersionsExceeded => DiskError::MaxVersionsExceeded,
|
||||
e => DiskError::other(e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,6 +588,7 @@ impl From<rustfs_filemeta::Error> for StorageError {
|
||||
rustfs_filemeta::Error::FileVersionNotFound => StorageError::FileVersionNotFound,
|
||||
rustfs_filemeta::Error::FileCorrupt => StorageError::FileCorrupt,
|
||||
rustfs_filemeta::Error::Unexpected => StorageError::Unexpected,
|
||||
rustfs_filemeta::Error::MaxVersionsExceeded => StorageError::MaxVersionsExceeded,
|
||||
rustfs_filemeta::Error::Io(io_error) => io_error.into(),
|
||||
_ => StorageError::Io(std::io::Error::other(e)),
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ pub enum Error {
|
||||
#[error("Method not allowed")]
|
||||
MethodNotAllowed,
|
||||
|
||||
#[error("You've exceeded the limit on the number of versions you can create on this object")]
|
||||
MaxVersionsExceeded,
|
||||
|
||||
#[error("Unexpected error")]
|
||||
Unexpected,
|
||||
|
||||
@@ -86,6 +89,7 @@ impl PartialEq for Error {
|
||||
(Error::FileCorrupt, Error::FileCorrupt) => true,
|
||||
(Error::DoneForNow, Error::DoneForNow) => true,
|
||||
(Error::MethodNotAllowed, Error::MethodNotAllowed) => true,
|
||||
(Error::MaxVersionsExceeded, Error::MaxVersionsExceeded) => true,
|
||||
(Error::FileNotFound, Error::FileNotFound) => true,
|
||||
(Error::FileVersionNotFound, Error::FileVersionNotFound) => true,
|
||||
(Error::VolumeNotFound, Error::VolumeNotFound) => true,
|
||||
@@ -111,6 +115,7 @@ impl Clone for Error {
|
||||
Error::FileCorrupt => Error::FileCorrupt,
|
||||
Error::DoneForNow => Error::DoneForNow,
|
||||
Error::MethodNotAllowed => Error::MethodNotAllowed,
|
||||
Error::MaxVersionsExceeded => Error::MaxVersionsExceeded,
|
||||
Error::VolumeNotFound => Error::VolumeNotFound,
|
||||
Error::Io(e) => Error::Io(std::io::Error::new(e.kind(), e.to_string())),
|
||||
Error::RmpSerdeDecode(s) => Error::RmpSerdeDecode(s.clone()),
|
||||
|
||||
+134
-14
@@ -34,11 +34,14 @@ use rustfs_utils::http::{
|
||||
};
|
||||
use s3s::header::X_AMZ_RESTORE;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(test)]
|
||||
use std::cell::Cell;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::hash::Hasher;
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
|
||||
use std::{collections::HashMap, io::Cursor};
|
||||
use time::OffsetDateTime;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
@@ -67,8 +70,46 @@ const _XL_FLAG_INLINE_DATA: u8 = 1 << 2;
|
||||
const META_DATA_READ_DEFAULT: usize = 4 << 10;
|
||||
const MSGP_UINT32_SIZE: usize = 5;
|
||||
|
||||
/// Max object versions per object, default is 10000
|
||||
const DEFAULT_OBJECT_MAX_VERSIONS: usize = 10000;
|
||||
/// Default max object versions per object, aligned with MinIO's default.
|
||||
pub const DEFAULT_OBJECT_MAX_VERSIONS: usize = if usize::BITS >= 64 {
|
||||
9_223_372_036_854_775_807
|
||||
} else {
|
||||
usize::MAX
|
||||
};
|
||||
|
||||
static OBJECT_MAX_VERSIONS: AtomicUsize = AtomicUsize::new(DEFAULT_OBJECT_MAX_VERSIONS);
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
static OBJECT_MAX_VERSIONS_OVERRIDE: Cell<Option<usize>> = const { Cell::new(None) };
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn object_max_versions() -> usize {
|
||||
#[cfg(test)]
|
||||
if let Some(limit) = OBJECT_MAX_VERSIONS_OVERRIDE.with(Cell::get) {
|
||||
return limit;
|
||||
}
|
||||
|
||||
OBJECT_MAX_VERSIONS.load(AtomicOrdering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn set_object_max_versions(limit: usize) -> Result<()> {
|
||||
if limit == 0 {
|
||||
return Err(Error::other("object max versions must be greater than 0"));
|
||||
}
|
||||
OBJECT_MAX_VERSIONS.store(limit, AtomicOrdering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn set_object_max_versions_override_for_test(limit: Option<usize>) -> Option<usize> {
|
||||
OBJECT_MAX_VERSIONS_OVERRIDE.with(|override_limit| {
|
||||
let previous = override_limit.get();
|
||||
override_limit.set(limit);
|
||||
previous
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the inline data map key for a version_id. "null" for null version.
|
||||
pub(crate) fn data_key_for_version(version_id: Option<Uuid>) -> String {
|
||||
@@ -460,18 +501,6 @@ impl FileMeta {
|
||||
return Err(Error::other("file meta version invalid"));
|
||||
}
|
||||
|
||||
// check max versions limit
|
||||
if self.versions.len() + 1 > DEFAULT_OBJECT_MAX_VERSIONS {
|
||||
return Err(Error::other(
|
||||
"You've exceeded the limit on the number of versions you can create on this object",
|
||||
));
|
||||
}
|
||||
|
||||
if self.versions.is_empty() {
|
||||
self.versions.push(FileMetaShallowVersion::try_from(version)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let vid = version.get_version_id();
|
||||
let vid_is_null = vid.is_none() || vid == Some(Uuid::nil());
|
||||
let existing_idx = if vid_is_null {
|
||||
@@ -490,6 +519,15 @@ impl FileMeta {
|
||||
return self.set_idx(fidx, version);
|
||||
}
|
||||
|
||||
if self.versions.len() >= object_max_versions() {
|
||||
return Err(Error::MaxVersionsExceeded);
|
||||
}
|
||||
|
||||
if self.versions.is_empty() {
|
||||
self.versions.push(FileMetaShallowVersion::try_from(version)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let new_shallow = FileMetaShallowVersion::try_from(version)?;
|
||||
let insert_pos = self
|
||||
.versions
|
||||
@@ -1330,6 +1368,88 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
struct ObjectMaxVersionsRestore {
|
||||
previous: Option<usize>,
|
||||
}
|
||||
|
||||
impl Drop for ObjectMaxVersionsRestore {
|
||||
fn drop(&mut self) {
|
||||
set_object_max_versions_override_for_test(self.previous);
|
||||
}
|
||||
}
|
||||
|
||||
fn with_object_max_versions_for_test<R>(limit: usize, test: impl FnOnce() -> R) -> R {
|
||||
let previous = set_object_max_versions_override_for_test(Some(limit));
|
||||
let _restore = ObjectMaxVersionsRestore { previous };
|
||||
test()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_version_filemata_rejects_new_version_above_configured_limit() {
|
||||
with_object_max_versions_for_test(2, || {
|
||||
let mut fm = FileMeta::new();
|
||||
fm.add_version_filemata(valid_object_version(Uuid::from_u128(1), vec![10, 20]))
|
||||
.expect("add first version within limit");
|
||||
fm.add_version_filemata(valid_object_version(Uuid::from_u128(2), vec![10, 20]))
|
||||
.expect("add second version at limit");
|
||||
|
||||
let err = fm
|
||||
.add_version_filemata(valid_object_version(Uuid::from_u128(3), vec![10, 20]))
|
||||
.expect_err("new version above limit must fail");
|
||||
|
||||
assert_eq!(err, Error::MaxVersionsExceeded);
|
||||
assert_eq!(fm.versions.len(), 2, "failed insert must not mutate version list");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_version_filemata_allows_same_version_replacement_at_limit() {
|
||||
with_object_max_versions_for_test(2, || {
|
||||
let mut fm = FileMeta::new();
|
||||
let target = Uuid::from_u128(10);
|
||||
fm.add_version_filemata(valid_object_version(target, vec![10, 20]))
|
||||
.expect("add target version");
|
||||
fm.add_version_filemata(valid_object_version(Uuid::from_u128(20), vec![10, 20]))
|
||||
.expect("add peer version at limit");
|
||||
|
||||
fm.add_version_filemata(valid_object_version(target, vec![30, 40]))
|
||||
.expect("same version replacement at limit must succeed");
|
||||
|
||||
assert_eq!(fm.versions.len(), 2);
|
||||
let replaced = fm
|
||||
.versions
|
||||
.iter()
|
||||
.find(|version| version.header.version_id == Some(target))
|
||||
.expect("target version must remain present")
|
||||
.parse_version_meta()
|
||||
.expect("parse replaced version");
|
||||
assert_eq!(replaced.object.expect("object version").part_sizes, vec![30, 40]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_version_allows_null_version_replacement_at_limit() {
|
||||
with_object_max_versions_for_test(1, || {
|
||||
let mut fm = FileMeta::new();
|
||||
let mut first = FileInfo::new("object", 2, 2);
|
||||
first.mod_time = Some(OffsetDateTime::now_utc());
|
||||
first.version_id = None;
|
||||
fm.add_version(first).expect("add initial null version");
|
||||
|
||||
let mut replacement = FileInfo::new("object", 2, 2);
|
||||
replacement.mod_time = Some(OffsetDateTime::now_utc());
|
||||
replacement.version_id = None;
|
||||
replacement.size = 42;
|
||||
fm.add_version(replacement)
|
||||
.expect("null version replacement at limit must succeed");
|
||||
|
||||
assert_eq!(fm.versions.len(), 1);
|
||||
assert_eq!(fm.versions[0].header.version_id, Some(Uuid::nil()));
|
||||
let replaced = fm.versions[0].parse_version_meta().expect("parse null replacement");
|
||||
assert_eq!(replaced.object.expect("object version").size, 42);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_version_filemata_uses_canonical_equal_time_order() {
|
||||
let mod_time = OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("valid test timestamp");
|
||||
|
||||
@@ -37,6 +37,8 @@ fn segment_proof() -> SegmentInvalidationProof {
|
||||
key_format: envelope.key_format,
|
||||
baseline_scan_plan_digest: envelope.baseline_scan_plan_digest,
|
||||
process_epoch: envelope.process_epoch,
|
||||
generation_start: envelope.generation_start,
|
||||
generation_end: envelope.generation_end,
|
||||
durable_producer_identity: true,
|
||||
invalidation_domain: SegmentInvalidationDomain::LocalSingleSet,
|
||||
distributed_ec_invalidation: false,
|
||||
@@ -126,6 +128,20 @@ fn segment_observation_trusted_proposal_requires_identity_and_complete_producer_
|
||||
Err(SegmentInvalidationError::InvalidProof)
|
||||
);
|
||||
|
||||
let mut wrong_generation_start = proof.clone();
|
||||
wrong_generation_start.generation_start = wrong_generation_start.generation_start.saturating_sub(1);
|
||||
assert_eq!(
|
||||
admit_segment_invalidation(&envelope, &wrong_generation_start, ["hot/one"]),
|
||||
Err(SegmentInvalidationError::InvalidProof)
|
||||
);
|
||||
|
||||
let mut wrong_generation_end = proof.clone();
|
||||
wrong_generation_end.generation_end = wrong_generation_end.generation_end.saturating_add(1);
|
||||
assert_eq!(
|
||||
admit_segment_invalidation(&envelope, &wrong_generation_end, ["hot/one"]),
|
||||
Err(SegmentInvalidationError::InvalidProof)
|
||||
);
|
||||
|
||||
let mut no_durable_identity = proof.clone();
|
||||
no_durable_identity.durable_producer_identity = false;
|
||||
assert_eq!(
|
||||
|
||||
@@ -77,6 +77,8 @@ pub struct SegmentInvalidationProof {
|
||||
pub key_format: u16,
|
||||
pub baseline_scan_plan_digest: DataUsageScanPlanDigest,
|
||||
pub process_epoch: String,
|
||||
pub generation_start: u64,
|
||||
pub generation_end: u64,
|
||||
pub durable_producer_identity: bool,
|
||||
pub invalidation_domain: SegmentInvalidationDomain,
|
||||
pub distributed_ec_invalidation: bool,
|
||||
@@ -108,11 +110,15 @@ fn validate_segment_invalidation_proof(
|
||||
|| envelope.baseline_scan_plan_digest != proof.baseline_scan_plan_digest
|
||||
|| envelope.process_epoch.is_empty()
|
||||
|| envelope.process_epoch != proof.process_epoch
|
||||
|| envelope.generation_start != proof.generation_start
|
||||
|| envelope.generation_end != proof.generation_end
|
||||
|| !proof.durable_producer_identity
|
||||
|| !proof.cold_zero_walk_oracle
|
||||
|| (proof.invalidation_domain == SegmentInvalidationDomain::DistributedEc && !proof.distributed_ec_invalidation)
|
||||
|| envelope.generation_start == 0
|
||||
|| envelope.generation_end < envelope.generation_start
|
||||
|| proof.generation_start == 0
|
||||
|| proof.generation_end < proof.generation_start
|
||||
|| envelope.restart_gap
|
||||
|| envelope.overflow
|
||||
|| !SegmentInvalidationProducer::REQUIRED
|
||||
@@ -190,6 +196,8 @@ mod tests {
|
||||
key_format: envelope.key_format,
|
||||
baseline_scan_plan_digest: envelope.baseline_scan_plan_digest,
|
||||
process_epoch: envelope.process_epoch,
|
||||
generation_start: envelope.generation_start,
|
||||
generation_end: envelope.generation_end,
|
||||
durable_producer_identity: true,
|
||||
invalidation_domain: SegmentInvalidationDomain::LocalSingleSet,
|
||||
distributed_ec_invalidation: false,
|
||||
@@ -241,6 +249,20 @@ mod tests {
|
||||
Err(SegmentInvalidationError::InvalidProof)
|
||||
);
|
||||
|
||||
let mut wrong_generation_start = proof.clone();
|
||||
wrong_generation_start.generation_start = wrong_generation_start.generation_start.saturating_sub(1);
|
||||
assert_eq!(
|
||||
admit_segment_invalidation(&envelope, &wrong_generation_start, ["hot/one"]),
|
||||
Err(SegmentInvalidationError::InvalidProof)
|
||||
);
|
||||
|
||||
let mut wrong_generation_end = proof.clone();
|
||||
wrong_generation_end.generation_end = wrong_generation_end.generation_end.saturating_add(1);
|
||||
assert_eq!(
|
||||
admit_segment_invalidation(&envelope, &wrong_generation_end, ["hot/one"]),
|
||||
Err(SegmentInvalidationError::InvalidProof)
|
||||
);
|
||||
|
||||
let mut no_durable_identity = proof.clone();
|
||||
no_durable_identity.durable_producer_identity = false;
|
||||
assert_eq!(
|
||||
|
||||
@@ -125,6 +125,7 @@ const EXTERNAL_COMPATIBLE_SUFFIXES: &[&str] = &[
|
||||
"ACCESS_KEY",
|
||||
"ACCESS_KEY_FILE",
|
||||
"ADDRESS",
|
||||
"API_OBJECT_MAX_VERSIONS",
|
||||
"API_XFF_HEADER",
|
||||
"AUDIT_WEBHOOK_AUTH_TOKEN",
|
||||
"AUDIT_WEBHOOK_CLIENT_CERT",
|
||||
@@ -900,4 +901,15 @@ mod tests {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_env_compat_includes_api_object_max_versions() {
|
||||
let report =
|
||||
build_external_env_compat_report_from_entries([("MINIO_API_OBJECT_MAX_VERSIONS".to_string(), "50000".to_string())]);
|
||||
|
||||
assert_eq!(
|
||||
report.mapped_pairs,
|
||||
vec![("MINIO_API_OBJECT_MAX_VERSIONS".to_string(), "RUSTFS_API_OBJECT_MAX_VERSIONS".to_string())]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user