fix(cache): harden object data cache coordination (#5004)

* fix(cache): enforce projected entry capacity

Refs: rustfs/backlog#1335

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

* fix(cache): fence identity budget eviction by generation

Refs rustfs/backlog#1334.

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

* fix(cache): fence clear against concurrent fills

Refs rustfs/backlog#1333

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

* fix(cache): linearize memory reservation claims

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

* fix(cache): retain allocation memory claims

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

* fix(cache): publish memory snapshots by epoch

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

* fix(cache): coordinate cold object fills

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

* fix(ecstore): fence metadata cache transition races

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
houseme
2026-07-18 22:37:10 +08:00
committed by GitHub
parent 4faea7fcbc
commit 15f4e75870
39 changed files with 9930 additions and 538 deletions
@@ -12,7 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::index::{ObjectDataCacheIndexInsertResult, ObjectDataCacheKeySet, ObjectDataCacheKeyToken};
use crate::index::{
ObjectDataCacheGeneration, ObjectDataCacheGenerationalInsertResult, ObjectDataCacheIndexInsertResult, ObjectDataCacheKeySet,
ObjectDataCacheKeyToken,
};
use crate::key::{ObjectDataCacheIdentity, ObjectDataCacheKey};
use starshard::{AsyncShardedHashMap, DEFAULT_SHARDS, SnapshotMode};
use std::collections::hash_map::RandomState;
@@ -56,6 +59,21 @@ impl StarshardIdentityIndex {
key: ObjectDataCacheKey,
token: ObjectDataCacheKeyToken,
) -> ObjectDataCacheIndexInsertResult {
let generation = u64::try_from(token).unwrap_or(u64::MAX);
match self.insert_generation(identity, key, generation).await {
ObjectDataCacheGenerationalInsertResult::Inserted { evicted } => ObjectDataCacheIndexInsertResult::Inserted {
evicted_keys: evicted.into_iter().map(|entry| entry.key).collect(),
},
ObjectDataCacheGenerationalInsertResult::Duplicate => ObjectDataCacheIndexInsertResult::Duplicate,
}
}
pub(crate) async fn insert_generation(
&self,
identity: ObjectDataCacheIdentity,
key: ObjectDataCacheKey,
generation: ObjectDataCacheGeneration,
) -> ObjectDataCacheGenerationalInsertResult {
let max_keys = self.max_keys_per_identity;
loop {
let mut outcome = None;
@@ -65,7 +83,7 @@ impl StarshardIdentityIndex {
let _ = self
.by_object
.compute_if_present(&identity, move |mut key_set| {
let result = key_set.insert(key, token, max_keys);
let result = key_set.insert_generation(key, generation, max_keys);
// Insert never empties the set (it either dedups or
// bounded-evicts and adds the new key), but keep the
// guard so an already-empty entry is not republished.
@@ -80,7 +98,7 @@ impl StarshardIdentityIndex {
// Identity not tracked yet: publish a fresh single-key set.
let mut fresh = ObjectDataCacheKeySet::default();
let result = fresh.insert(key.clone(), token, max_keys);
let result = fresh.insert_generation(key.clone(), generation, max_keys);
let final_set = self.by_object.compute_if_absent(identity.clone(), move || fresh).await;
if final_set.contains(&key) {
return result;
@@ -146,6 +164,16 @@ impl StarshardIdentityIndex {
identity: &ObjectDataCacheIdentity,
key: &ObjectDataCacheKey,
token: ObjectDataCacheKeyToken,
) -> bool {
self.remove_generation(identity, key, u64::try_from(token).unwrap_or(u64::MAX))
.await
}
pub(crate) async fn remove_generation(
&self,
identity: &ObjectDataCacheIdentity,
key: &ObjectDataCacheKey,
generation: ObjectDataCacheGeneration,
) -> bool {
let mut removed = false;
{
@@ -153,7 +181,7 @@ impl StarshardIdentityIndex {
let _ = self
.by_object
.compute_if_present(identity, move |mut key_set| {
*removed = key_set.remove_evicted_key(key, token);
*removed = key_set.remove_generation(key, generation);
(!key_set.is_empty()).then_some(key_set)
})
.await;
@@ -169,6 +197,18 @@ impl StarshardIdentityIndex {
.is_some_and(|key_set| key_set.contains(key))
}
pub(crate) async fn contains_generation(
&self,
identity: &ObjectDataCacheIdentity,
key: &ObjectDataCacheKey,
generation: ObjectDataCacheGeneration,
) -> bool {
self.by_object
.get(identity)
.await
.is_some_and(|key_set| key_set.contains_generation(key, generation))
}
/// Returns the number of tracked identities.
#[cfg(test)]
pub(crate) async fn identity_count(&self) -> usize {