Files
rustfs/crates/object-data-cache/src/error.rs
T
houseme 904554b417 fix(object-data-cache): make memory container-aware and bound cache config (#4655)
* fix(object-data-cache): bound cache config and make memory container-aware

Harden the object data cache configuration path so a single bad input
degrades to the disabled adapter instead of OOM-killing pods, panicking
at boot, or silently mis-sizing the cache (backlog#1110/1113/1114/1115/
1127/1140/1130).

- ODC-05: resolve capacity and the fill gate from the effective
  (container-aware) memory. Prefer sysinfo cgroup_limits() and use
  min(host, cgroup) as the total plus cgroup-derived availability, falling
  back to host values when no cgroup limit constrains. Log the resolved
  capacity and its basis once at startup.
- ODC-08: cap ttl/time_to_idle at 30 days in validate() so an unbounded
  Duration can no longer trip moka's ~1000-year builder assertion.
- ODC-09: drop the max_entry_bytes floor from the derived-capacity clamp so
  MAX_ENTRY_BYTES can no longer inflate total capacity above the safety
  clamp; reject an entry larger than the resolved capacity instead.
- ODC-10: require an explicit max_bytes to clear max_entry_bytes plus the
  weigher overhead so a fillable-but-unretainable cache is rejected.
- ODC-22: reject max_entry_bytes at/above the u32 weigher boundary.
- ODC-35: warn (not reject) when time_to_idle exceeds ttl and document the
  min(ttl, time_to_idle) expiry interaction.
- ODC-25: give numeric env overrides two-valued semantics via a new
  rustfs_utils::get_env_parse_outcome; a malformed value now disables the
  whole cache with one aggregated warning instead of silently keeping
  defaults.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(object-data-cache): require identity_keys_max of at least 2

The identity index admits a new key by evicting the oldest one, so a
budget of 1 evicts the previous key on every fill and can never hold two
live keys of one object at once — a versioned bucket alternating two
versions then hits a permanent 0% hit rate.

Reject the degenerate value at config validation time, where the adapter
turns it into a disabled cache with a warning, since the bounded-eviction
policy cannot rescue it at runtime.

Handed off from the identity-index batch (backlog#1128), which changed the
overflow policy but could not touch validate().

Refs: backlog#1115, backlog#1128

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(object-data-cache): let a zero free-memory floor opt out of the gate

Making the memory gate container-aware turned it live in CI: the runners
are Kubernetes pods, so the gate now reads the pod's cgroup free memory,
finds it below the 20% floor, and refuses the fill. Tests that assert a
fill succeeds then failed on CI while passing on a developer host, which
has no cgroup and falls back to host memory.

The gate is behaving correctly — the tests were the ones depending on a
live memory reading. Treat min_free_memory_percent = 0 as a deliberate
opt-out rather than an invalid value: allows_fill returns early before it
touches any snapshot, so admission becomes independent of where the suite
runs. Operators gain the same escape hatch.

Every test that requires a fill to succeed now sets the floor to 0. The
tests that exercise the gate keep it enabled via memory_gated_config, so
the ODC-05 coverage they provide is preserved rather than short-circuited.

Refs: backlog#1110

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(object-data-cache): opt the usecase fill tests out of the memory gate

The previous commit exempted the fill-dependent tests it could find, but
missed the six adapters built inside object_usecase.rs. Fixing the first
batch moved nextest's fail-fast point forward and CI surfaced them:
build_get_object_body_with_cache_materializes_once_and_hits_later and
..._uses_cached_body_without_reader_preread both assert a fill lands, and
both ran with the default 20% free-memory floor.

Set the floor to 0 on all six, matching the sibling test modules. Verified
by pinning the gate's snapshot to 0% available — harsher than any CI pod —
and confirming these tests still pass, which shows the exemption path never
reads memory at all.

An exhaustive sweep over every fill-enabled ObjectDataCacheConfig in the
tree now shows no remaining site: the only unexempted ones are
fill_enabled()'s matches! arm and two adapter tests that assert the adapter
is disabled and therefore never fill.

Refs: backlog#1110

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 09:17:32 +00:00

84 lines
3.6 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 thiserror::Error;
/// Configuration errors for the object data cache engine.
#[derive(Debug, Error, PartialEq, Eq)]
pub enum ObjectDataCacheConfigError {
/// The configured memory percentage exceeded the supported range.
#[error("object data cache max_memory_percent must be in 1..=100")]
InvalidMaxMemoryPercent,
/// The configured entry size exceeded the supported range.
#[error("object data cache max_entry_bytes must be greater than 0")]
ZeroMaxEntryBytes,
/// The configured entry size exceeds the moka weigher's u32 accounting range.
#[error("object data cache max_entry_bytes must stay below 4 GiB so the capacity weigher does not under-count entries")]
MaxEntryBytesTooLarge,
/// The configured entry size cannot fit inside the explicit byte capacity.
#[error("object data cache max_entry_bytes plus weigher overhead must not exceed max_bytes")]
MaxEntryBytesExceedsMaxBytes,
/// The configured entry size exceeds the resolved (derived) cache capacity.
#[error("object data cache max_entry_bytes must not exceed the resolved cache capacity")]
MaxEntryBytesExceedsCapacity,
/// The configured time-to-live cannot be zero.
#[error("object data cache ttl_secs must be greater than 0")]
ZeroTimeToLiveSecs,
/// The configured time-to-live exceeds the supported upper bound.
#[error("object data cache ttl_secs must not exceed 2592000 seconds (30 days)")]
TimeToLiveTooLarge,
/// The configured time-to-idle cannot be zero.
#[error("object data cache time_to_idle_secs must be greater than 0")]
ZeroTimeToIdleSecs,
/// The configured time-to-idle exceeds the supported upper bound.
#[error("object data cache time_to_idle_secs must not exceed 2592000 seconds (30 days)")]
TimeToIdleTooLarge,
/// The configured minimum free memory percentage exceeded the supported range.
#[error("object data cache min_free_memory_percent must be in 1..=100")]
InvalidMinFreeMemoryPercent,
/// The configured fill concurrency per CPU exceeded the supported range.
#[error("object data cache fill_concurrency_per_cpu must be greater than 0")]
ZeroFillConcurrencyPerCpu,
/// The configured fill concurrency maximum exceeded the supported range.
#[error("object data cache fill_concurrency_max must be greater than 0")]
ZeroFillConcurrencyMax,
/// The configured fill concurrency bounds are internally inconsistent.
#[error("object data cache fill_concurrency_max must be at least fill_concurrency_per_cpu")]
FillConcurrencyMaxTooSmall,
/// The configured identity key-set cap exceeded the supported range.
#[error("object data cache identity_keys_max must be greater than 0")]
ZeroIdentityKeysMax,
/// A single-key identity budget evicts the previous key on every fill.
#[error("object data cache identity_keys_max must be at least 2")]
IdentityKeysMaxTooSmall,
/// Failed to resolve a non-zero cache capacity from the runtime environment.
#[error("object data cache could not resolve a positive max capacity")]
ZeroResolvedMaxBytes,
}