mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-06 13:27:43 +00:00
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:
@@ -14,8 +14,10 @@
|
||||
|
||||
use crate::backend::ObjectDataCacheBackendKind;
|
||||
use crate::config::ObjectDataCacheConfig;
|
||||
use crate::entry::projected_weight;
|
||||
use crate::error::ObjectDataCacheConfigError;
|
||||
use crate::key::{ObjectDataCacheBodyVariant, ObjectDataCacheIdentity, ObjectDataCacheKey};
|
||||
use crate::memory::ObjectDataCacheMemoryReservation;
|
||||
use crate::metrics::{
|
||||
describe_metrics_once, publish_cache_state, record_fill_result, record_hit_bytes, record_invalidation, record_lookup_result,
|
||||
record_plan_decision,
|
||||
@@ -27,6 +29,52 @@ use bytes::Bytes;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::Instant;
|
||||
use tokio::sync::OwnedSemaphorePermit;
|
||||
|
||||
/// Admission token for one body allocation performed before a cold cache fill.
|
||||
#[derive(Debug)]
|
||||
pub struct ObjectDataCacheBodyReservation {
|
||||
pub(crate) memory: ObjectDataCacheMemoryReservation,
|
||||
pub(crate) permit: OwnedSemaphorePermit,
|
||||
pub(crate) fill_generation: crate::moka_backend::FillGenerationGuard,
|
||||
pub(crate) key: ObjectDataCacheKey,
|
||||
pub(crate) expected_size: u64,
|
||||
}
|
||||
|
||||
/// A materialized body whose allocation owns its memory claim until the last
|
||||
/// `Bytes` clone is dropped.
|
||||
#[derive(Debug)]
|
||||
pub struct ObjectDataCacheReservedBody {
|
||||
pub(crate) bytes: Bytes,
|
||||
pub(crate) permit: OwnedSemaphorePermit,
|
||||
pub(crate) fill_generation: crate::moka_backend::FillGenerationGuard,
|
||||
pub(crate) key: ObjectDataCacheKey,
|
||||
pub(crate) expected_size: u64,
|
||||
}
|
||||
|
||||
impl ObjectDataCacheBodyReservation {
|
||||
/// Attaches this reservation to a newly materialized body.
|
||||
pub fn wrap_bytes(self, bytes: Bytes) -> ObjectDataCacheReservedBody {
|
||||
ObjectDataCacheReservedBody {
|
||||
bytes: self.memory.wrap_bytes(bytes),
|
||||
permit: self.permit,
|
||||
fill_generation: self.fill_generation,
|
||||
key: self.key,
|
||||
expected_size: self.expected_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectDataCacheReservedBody {
|
||||
/// Returns a clone that shares the reservation-owning allocation.
|
||||
pub fn bytes(&self) -> Bytes {
|
||||
self.bytes.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn into_parts(self) -> (Bytes, OwnedSemaphorePermit, crate::moka_backend::FillGenerationGuard) {
|
||||
(self.bytes, self.permit, self.fill_generation)
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimum spacing between cache-state gauge publishes. Moka's `entry_count`
|
||||
/// and `weighted_size` are cross-segment approximations that only settle after
|
||||
@@ -45,6 +93,9 @@ pub struct ObjectDataCache {
|
||||
backend: ObjectDataCacheBackendKind,
|
||||
config: Arc<ObjectDataCacheConfig>,
|
||||
stats: Arc<ObjectDataCacheStats>,
|
||||
/// Resolved Moka weighted capacity used by the planner's exact key-aware
|
||||
/// admission check. Zero for the disabled backend.
|
||||
max_capacity: u64,
|
||||
/// Effective fill ceiling in bytes: a body larger than this is planned
|
||||
/// `SkipTooLarge` even when it fits `max_entry_bytes`. The app layer sets it
|
||||
/// to `min(max_entry_bytes, seek-support threshold, 64 MiB buffer cap)` so a
|
||||
@@ -72,6 +123,7 @@ impl ObjectDataCache {
|
||||
backend: ObjectDataCacheBackendKind::Noop(NoopBackend),
|
||||
config,
|
||||
stats,
|
||||
max_capacity: 0,
|
||||
fill_ceiling_bytes: 0,
|
||||
created_at: Instant::now(),
|
||||
last_entry_publish_ms: AtomicU64::new(0),
|
||||
@@ -83,16 +135,19 @@ impl ObjectDataCache {
|
||||
describe_metrics_once();
|
||||
config.validate()?;
|
||||
let stats = Arc::new(ObjectDataCacheStats::default());
|
||||
let backend = if config.is_disabled() {
|
||||
ObjectDataCacheBackendKind::Noop(NoopBackend)
|
||||
let (backend, max_capacity) = if config.is_disabled() {
|
||||
(ObjectDataCacheBackendKind::Noop(NoopBackend), 0)
|
||||
} else {
|
||||
ObjectDataCacheBackendKind::Moka(Box::new(MokaBackend::new(&config, Arc::clone(&stats))?))
|
||||
let backend = MokaBackend::new(&config, Arc::clone(&stats))?;
|
||||
let max_capacity = backend.max_capacity();
|
||||
(ObjectDataCacheBackendKind::Moka(Box::new(backend)), max_capacity)
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
backend,
|
||||
config: Arc::new(config),
|
||||
stats,
|
||||
max_capacity,
|
||||
fill_ceiling_bytes: 0,
|
||||
created_at: Instant::now(),
|
||||
last_entry_publish_ms: AtomicU64::new(0),
|
||||
@@ -121,14 +176,26 @@ impl ObjectDataCache {
|
||||
|
||||
/// Produces a lightweight GET plan from request metadata.
|
||||
pub fn plan_get(&self, request: ObjectDataCacheGetRequest<'_>) -> ObjectDataCacheGetPlan {
|
||||
self.plan_get_inner(request, true)
|
||||
}
|
||||
|
||||
/// Rebuilds a plan for identity revalidation without counting another GET.
|
||||
#[doc(hidden)]
|
||||
pub fn plan_get_untracked(&self, request: ObjectDataCacheGetRequest<'_>) -> ObjectDataCacheGetPlan {
|
||||
self.plan_get_inner(request, false)
|
||||
}
|
||||
|
||||
fn plan_get_inner(&self, request: ObjectDataCacheGetRequest<'_>, record_metric: bool) -> ObjectDataCacheGetPlan {
|
||||
if self.config.is_disabled() {
|
||||
record_plan_decision(
|
||||
self.backend.as_metric_label(),
|
||||
self.config.mode,
|
||||
"disabled",
|
||||
"mode_disabled",
|
||||
request.size,
|
||||
);
|
||||
if record_metric {
|
||||
record_plan_decision(
|
||||
self.backend.as_metric_label(),
|
||||
self.config.mode,
|
||||
"disabled",
|
||||
"mode_disabled",
|
||||
request.size,
|
||||
);
|
||||
}
|
||||
return ObjectDataCacheGetPlan::Disabled;
|
||||
}
|
||||
|
||||
@@ -137,24 +204,34 @@ impl ObjectDataCache {
|
||||
// in-memory GET fill limits could never fill, so admitting it here would
|
||||
// report it "eligible" while it kept a permanent 0% hit rate.
|
||||
if request.size > self.effective_size_ceiling() {
|
||||
record_plan_decision(self.backend.as_metric_label(), self.config.mode, "skip", "too_large", request.size);
|
||||
if record_metric {
|
||||
record_plan_decision(self.backend.as_metric_label(), self.config.mode, "skip", "too_large", request.size);
|
||||
}
|
||||
return ObjectDataCacheGetPlan::SkipTooLarge;
|
||||
}
|
||||
|
||||
record_plan_decision(self.backend.as_metric_label(), self.config.mode, "cacheable", "eligible", request.size);
|
||||
|
||||
ObjectDataCacheGetPlan::Cacheable {
|
||||
key: ObjectDataCacheKey::with_write_anchors(
|
||||
request.bucket,
|
||||
request.object,
|
||||
request.version_id.as_deref(),
|
||||
request.etag,
|
||||
request.size,
|
||||
request.data_dir_u128,
|
||||
request.mod_time_unix_nanos,
|
||||
request.body_variant,
|
||||
),
|
||||
let key = ObjectDataCacheKey::with_write_anchors(
|
||||
request.bucket,
|
||||
request.object,
|
||||
request.version_id.as_deref(),
|
||||
request.etag,
|
||||
request.size,
|
||||
request.data_dir_u128,
|
||||
request.mod_time_unix_nanos,
|
||||
request.body_variant,
|
||||
);
|
||||
if projected_weight(&key, request.size) > self.max_capacity {
|
||||
if record_metric {
|
||||
record_plan_decision(self.backend.as_metric_label(), self.config.mode, "skip", "too_large", request.size);
|
||||
}
|
||||
return ObjectDataCacheGetPlan::SkipTooLarge;
|
||||
}
|
||||
|
||||
if record_metric {
|
||||
record_plan_decision(self.backend.as_metric_label(), self.config.mode, "cacheable", "eligible", request.size);
|
||||
}
|
||||
|
||||
ObjectDataCacheGetPlan::Cacheable { key }
|
||||
}
|
||||
|
||||
/// Looks up an object body from the configured backend.
|
||||
@@ -192,6 +269,61 @@ impl ObjectDataCache {
|
||||
lookup
|
||||
}
|
||||
|
||||
/// Performs an internal second-chance lookup without recording another
|
||||
/// request lookup. Callers must have already performed the authoritative
|
||||
/// lookup for the current GET.
|
||||
#[doc(hidden)]
|
||||
pub async fn peek_body_untracked(&self, plan: &ObjectDataCacheGetPlan) -> ObjectDataCacheLookup {
|
||||
match &self.backend {
|
||||
ObjectDataCacheBackendKind::Noop(backend) => backend.lookup_body(plan).await,
|
||||
ObjectDataCacheBackendKind::Moka(backend) => backend.lookup_body(plan).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reserves memory and a fill slot before allocating a cold-fill body.
|
||||
pub fn reserve_body(&self, plan: &ObjectDataCacheGetPlan) -> Option<ObjectDataCacheBodyReservation> {
|
||||
if !self.config.fill_enabled() {
|
||||
return None;
|
||||
}
|
||||
match &self.backend {
|
||||
ObjectDataCacheBackendKind::Noop(_) => None,
|
||||
ObjectDataCacheBackendKind::Moka(backend) => backend.reserve_body(plan),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fills from a body admitted before allocation.
|
||||
pub async fn fill_reserved_body(
|
||||
&self,
|
||||
plan: &ObjectDataCacheGetPlan,
|
||||
body: ObjectDataCacheReservedBody,
|
||||
) -> ObjectDataCacheFillResult {
|
||||
let fill_bytes = u64::try_from(body.bytes.len()).unwrap_or(u64::MAX);
|
||||
let fill_start = Instant::now();
|
||||
let result = match &self.backend {
|
||||
ObjectDataCacheBackendKind::Noop(_) => ObjectDataCacheFillResult::SkippedDisabled,
|
||||
ObjectDataCacheBackendKind::Moka(backend) => backend.fill_reserved_body(plan, body).await,
|
||||
};
|
||||
let (recorded_bytes, duration) = match result {
|
||||
ObjectDataCacheFillResult::Inserted => {
|
||||
self.stats.record_fill();
|
||||
self.refresh_entry_count();
|
||||
(fill_bytes, Some(fill_start.elapsed().as_secs_f64()))
|
||||
}
|
||||
ObjectDataCacheFillResult::SkippedInvalidationRace | ObjectDataCacheFillResult::SkippedIdentityOverflow => {
|
||||
(fill_bytes, Some(fill_start.elapsed().as_secs_f64()))
|
||||
}
|
||||
_ => (0, None),
|
||||
};
|
||||
record_fill_result(
|
||||
self.backend.as_metric_label(),
|
||||
self.config.mode,
|
||||
result.as_metric_label(),
|
||||
recorded_bytes,
|
||||
duration,
|
||||
);
|
||||
result
|
||||
}
|
||||
|
||||
/// Attempts to fill the cache body for the current plan.
|
||||
pub async fn fill_body(&self, plan: &ObjectDataCacheGetPlan, bytes: Bytes) -> ObjectDataCacheFillResult {
|
||||
let fill_bytes = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
|
||||
@@ -434,6 +566,16 @@ pub enum ObjectDataCacheGetPlan {
|
||||
},
|
||||
}
|
||||
|
||||
impl ObjectDataCacheGetPlan {
|
||||
/// Returns the stable cache key for a cacheable plan.
|
||||
pub fn key(&self) -> Option<&ObjectDataCacheKey> {
|
||||
match self {
|
||||
Self::Cacheable { key } => Some(key),
|
||||
Self::Disabled | Self::SkipTooLarge => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of a cache lookup attempt.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ObjectDataCacheLookup {
|
||||
@@ -863,6 +1005,80 @@ mod tests {
|
||||
assert!(matches!(lookup, ObjectDataCacheLookup::Miss));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reserved_body_is_bound_to_origin_cache_and_plan() {
|
||||
let cache_a = fill_enabled_cache();
|
||||
let cache_b = fill_enabled_cache();
|
||||
let plan = cache_a.plan_get(plain_request("bucket", "object", "etag", 5));
|
||||
let wrong_plan = cache_a.plan_get(plain_request("bucket", "other", "etag", 5));
|
||||
|
||||
let wrong_plan_body = cache_a
|
||||
.reserve_body(&plan)
|
||||
.expect("the original plan should be admitted")
|
||||
.wrap_bytes(Bytes::from_static(b"hello"));
|
||||
assert_eq!(
|
||||
cache_a.fill_reserved_body(&wrong_plan, wrong_plan_body).await,
|
||||
ObjectDataCacheFillResult::SkippedNotCacheable
|
||||
);
|
||||
|
||||
let cross_cache_body = cache_a
|
||||
.reserve_body(&plan)
|
||||
.expect("the original cache should admit another reservation")
|
||||
.wrap_bytes(Bytes::from_static(b"hello"));
|
||||
assert_eq!(
|
||||
cache_b.fill_reserved_body(&plan, cross_cache_body).await,
|
||||
ObjectDataCacheFillResult::SkippedNotCacheable
|
||||
);
|
||||
|
||||
let wrong_size_body = cache_a
|
||||
.reserve_body(&plan)
|
||||
.expect("the original plan should still be admitted")
|
||||
.wrap_bytes(Bytes::from_static(b"oops"));
|
||||
assert_eq!(
|
||||
cache_a.fill_reserved_body(&plan, wrong_size_body).await,
|
||||
ObjectDataCacheFillResult::SkippedSizeMismatch
|
||||
);
|
||||
assert_eq!(cache_a.stats().fills, 0, "rejected reservations must not count as successful fills");
|
||||
assert!(matches!(cache_a.lookup_body(&plan).await, ObjectDataCacheLookup::Miss));
|
||||
assert!(matches!(cache_a.lookup_body(&wrong_plan).await, ObjectDataCacheLookup::Miss));
|
||||
assert!(matches!(cache_b.lookup_body(&plan).await, ObjectDataCacheLookup::Miss));
|
||||
|
||||
let body = cache_a
|
||||
.reserve_body(&plan)
|
||||
.expect("the matching reservation should be admitted")
|
||||
.wrap_bytes(Bytes::from_static(b"hello"));
|
||||
assert_eq!(cache_a.fill_reserved_body(&plan, body).await, ObjectDataCacheFillResult::Inserted);
|
||||
assert_eq!(cache_a.stats().fills, 1);
|
||||
assert_eq!(cache_a.lookup_body(&plan).await, ObjectDataCacheLookup::Hit(Bytes::from_static(b"hello")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reserved_size_mismatch_records_fill_outcome_without_bytes() {
|
||||
let cache = fill_enabled_cache();
|
||||
let metrics = capture_metrics(|| async {
|
||||
let plan = cache.plan_get(plain_request("bucket", "object", "etag", 5));
|
||||
let body = cache
|
||||
.reserve_body(&plan)
|
||||
.expect("the plan should be admitted before materialization")
|
||||
.wrap_bytes(Bytes::from_static(b"oops"));
|
||||
assert_eq!(
|
||||
cache.fill_reserved_body(&plan, body).await,
|
||||
ObjectDataCacheFillResult::SkippedSizeMismatch
|
||||
);
|
||||
});
|
||||
|
||||
assert!(has_counter_with_label(
|
||||
&metrics,
|
||||
"rustfs_object_data_cache_fill_total",
|
||||
("result", "skipped_size_mismatch")
|
||||
));
|
||||
assert_eq!(
|
||||
counter_total(&metrics, "rustfs_object_data_cache_fill_bytes_total"),
|
||||
None,
|
||||
"a rejected reserved body must not record filled bytes"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_clamps_size_eligibility_to_fill_ceiling() {
|
||||
// ODC-24 (backlog#1129): a body in the gap between `max_entry_bytes` and
|
||||
@@ -895,6 +1111,34 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn planner_rejects_key_whose_projected_weight_exceeds_capacity() {
|
||||
let config = ObjectDataCacheConfig {
|
||||
mode: ObjectDataCacheMode::HitOnly,
|
||||
max_bytes: 8 * 1024,
|
||||
max_memory_percent: 0,
|
||||
max_entry_bytes: 4 * 1024,
|
||||
..ObjectDataCacheConfig::default()
|
||||
};
|
||||
let cache = ObjectDataCache::new(config).expect("capacity test config should initialize");
|
||||
// Body (4096) + fixed fields (bucket=1, version=null=4, etag=1) +
|
||||
// object (4026) + entry overhead (64) = 8192, exactly capacity.
|
||||
let boundary_object = "o".repeat(4026);
|
||||
assert!(matches!(
|
||||
cache.plan_get(plain_request("b", &boundary_object, "e", 4 * 1024)),
|
||||
ObjectDataCacheGetPlan::Cacheable { .. }
|
||||
));
|
||||
|
||||
// Body (4096) + fixed fields (bucket=1, version=null=4, etag=1) +
|
||||
// object (4027) + entry overhead (64) = 8193, one byte over capacity.
|
||||
let overweight_object = "o".repeat(4027);
|
||||
|
||||
assert_eq!(
|
||||
cache.plan_get(plain_request("b", &overweight_object, "e", 4 * 1024)),
|
||||
ObjectDataCacheGetPlan::SkipTooLarge
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_carries_mod_time_into_key() {
|
||||
// ODC-06: the resolved modification time must reach the key so two
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use crate::error::ObjectDataCacheConfigError;
|
||||
use crate::memory::{MemoryBasis, resolve_effective_memory};
|
||||
use crate::memory::{EffectiveMemory, MemoryBasis, resolve_effective_memory};
|
||||
use std::sync::Once;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -142,13 +142,21 @@ impl ObjectDataCacheConfig {
|
||||
/// Resolves the effective max capacity in bytes for the cache.
|
||||
pub fn resolved_max_bytes(&self) -> Result<u64, ObjectDataCacheConfigError> {
|
||||
if self.max_bytes > 0 {
|
||||
validate_entry_fits_capacity(
|
||||
self.max_bytes,
|
||||
self.max_entry_bytes,
|
||||
ObjectDataCacheConfigError::MaxEntryBytesExceedsMaxBytes,
|
||||
)?;
|
||||
return Ok(self.max_bytes);
|
||||
}
|
||||
|
||||
// Resolve capacity from the effective (container-aware) total memory so
|
||||
// a pod with a cgroup limit far below the node RAM does not size the
|
||||
// cache to the node.
|
||||
let effective = resolve_effective_memory();
|
||||
self.resolved_max_bytes_for_effective_memory(resolve_effective_memory())
|
||||
}
|
||||
|
||||
fn resolved_max_bytes_for_effective_memory(&self, effective: EffectiveMemory) -> Result<u64, ObjectDataCacheConfigError> {
|
||||
let total_memory = effective.total_bytes;
|
||||
let derived = total_memory.saturating_mul(u64::from(self.max_memory_percent)) / 100;
|
||||
let resolved = clamp_derived_max_bytes(derived, total_memory);
|
||||
@@ -159,10 +167,9 @@ impl ObjectDataCacheConfig {
|
||||
|
||||
// The derived capacity is no longer floored by `max_entry_bytes` (that
|
||||
// used to silently inflate the cache above the safety clamp). If a
|
||||
// single entry cannot fit, reject rather than inflate.
|
||||
if self.max_entry_bytes > resolved {
|
||||
return Err(ObjectDataCacheConfigError::MaxEntryBytesExceedsCapacity);
|
||||
}
|
||||
// single entry plus its weight overhead cannot fit, reject rather than
|
||||
// inflate.
|
||||
validate_entry_fits_capacity(resolved, self.max_entry_bytes, ObjectDataCacheConfigError::MaxEntryBytesExceedsCapacity)?;
|
||||
|
||||
log_resolved_capacity_once(resolved, total_memory, effective.basis);
|
||||
|
||||
@@ -186,8 +193,12 @@ impl ObjectDataCacheConfig {
|
||||
// An explicit capacity must leave room for a full entry plus the
|
||||
// weigher overhead, otherwise moka can never retain the entry while
|
||||
// fills still report success.
|
||||
if self.max_bytes > 0 && self.max_bytes < self.max_entry_bytes.saturating_add(ENTRY_WEIGHT_OVERHEAD_BYTES) {
|
||||
return Err(ObjectDataCacheConfigError::MaxEntryBytesExceedsMaxBytes);
|
||||
if self.max_bytes > 0 {
|
||||
validate_entry_fits_capacity(
|
||||
self.max_bytes,
|
||||
self.max_entry_bytes,
|
||||
ObjectDataCacheConfigError::MaxEntryBytesExceedsMaxBytes,
|
||||
)?;
|
||||
}
|
||||
|
||||
if self.ttl.is_zero() {
|
||||
@@ -249,6 +260,17 @@ impl ObjectDataCacheConfig {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_entry_fits_capacity(
|
||||
capacity: u64,
|
||||
max_entry_bytes: u64,
|
||||
error: ObjectDataCacheConfigError,
|
||||
) -> Result<(), ObjectDataCacheConfigError> {
|
||||
match max_entry_bytes.checked_add(ENTRY_WEIGHT_OVERHEAD_BYTES) {
|
||||
Some(required_capacity) if required_capacity <= capacity => Ok(()),
|
||||
_ => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
fn clamp_derived_max_bytes(derived: u64, total_memory: u64) -> u64 {
|
||||
let percent_cap = total_memory.saturating_mul(DEFAULT_DERIVED_MAX_MEMORY_PERCENT_CAP) / 100;
|
||||
let safe_cap = percent_cap.min(DEFAULT_DERIVED_MAX_BYTES_CAP);
|
||||
@@ -269,8 +291,12 @@ fn log_resolved_capacity_once(resolved_max_bytes: u64, effective_total_bytes: u6
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{DEFAULT_DERIVED_MAX_BYTES_CAP, ObjectDataCacheConfig, ObjectDataCacheMode, clamp_derived_max_bytes};
|
||||
use super::{
|
||||
DEFAULT_DERIVED_MAX_BYTES_CAP, ENTRY_WEIGHT_OVERHEAD_BYTES, ObjectDataCacheConfig, ObjectDataCacheMode,
|
||||
clamp_derived_max_bytes,
|
||||
};
|
||||
use crate::error::ObjectDataCacheConfigError;
|
||||
use crate::memory::{EffectiveMemory, MemoryBasis};
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
@@ -459,6 +485,87 @@ mod tests {
|
||||
assert_eq!(err, ObjectDataCacheConfigError::MaxEntryBytesExceedsCapacity);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derived_capacity_rejects_entry_equal_to_capacity() {
|
||||
const CAPACITY: u64 = 8 * 1024;
|
||||
let config = ObjectDataCacheConfig {
|
||||
max_bytes: 0,
|
||||
max_memory_percent: 10,
|
||||
max_entry_bytes: CAPACITY,
|
||||
..ObjectDataCacheConfig::default()
|
||||
};
|
||||
|
||||
let err = config
|
||||
.resolved_max_bytes_for_effective_memory(EffectiveMemory {
|
||||
total_bytes: CAPACITY * 10,
|
||||
available_bytes: CAPACITY * 10,
|
||||
basis: MemoryBasis::Host,
|
||||
})
|
||||
.expect_err("an entry equal to derived capacity leaves no room for its weight overhead");
|
||||
|
||||
assert_eq!(err, ObjectDataCacheConfigError::MaxEntryBytesExceedsCapacity);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_and_derived_capacity_share_entry_margin_matrix() {
|
||||
const CAPACITY: u64 = 8 * 1024;
|
||||
let effective_memory = EffectiveMemory {
|
||||
total_bytes: CAPACITY * 10,
|
||||
available_bytes: CAPACITY * 10,
|
||||
basis: MemoryBasis::Host,
|
||||
};
|
||||
|
||||
for (max_entry_bytes, fits) in [
|
||||
(CAPACITY - ENTRY_WEIGHT_OVERHEAD_BYTES, true),
|
||||
(CAPACITY - ENTRY_WEIGHT_OVERHEAD_BYTES + 1, false),
|
||||
(CAPACITY - 1, false),
|
||||
(CAPACITY, false),
|
||||
] {
|
||||
let explicit = ObjectDataCacheConfig {
|
||||
max_bytes: CAPACITY,
|
||||
max_memory_percent: 0,
|
||||
max_entry_bytes,
|
||||
..ObjectDataCacheConfig::default()
|
||||
};
|
||||
let derived = ObjectDataCacheConfig {
|
||||
max_bytes: 0,
|
||||
max_memory_percent: 10,
|
||||
max_entry_bytes,
|
||||
..ObjectDataCacheConfig::default()
|
||||
};
|
||||
|
||||
if fits {
|
||||
assert_eq!(explicit.validate(), Ok(()), "explicit max_entry_bytes={max_entry_bytes}");
|
||||
assert_eq!(
|
||||
explicit.resolved_max_bytes(),
|
||||
Ok(CAPACITY),
|
||||
"explicit resolved max_entry_bytes={max_entry_bytes}"
|
||||
);
|
||||
assert_eq!(
|
||||
derived.resolved_max_bytes_for_effective_memory(effective_memory),
|
||||
Ok(CAPACITY),
|
||||
"derived max_entry_bytes={max_entry_bytes}"
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
explicit.validate(),
|
||||
Err(ObjectDataCacheConfigError::MaxEntryBytesExceedsMaxBytes),
|
||||
"explicit max_entry_bytes={max_entry_bytes}"
|
||||
);
|
||||
assert_eq!(
|
||||
explicit.resolved_max_bytes(),
|
||||
Err(ObjectDataCacheConfigError::MaxEntryBytesExceedsMaxBytes),
|
||||
"explicit resolved max_entry_bytes={max_entry_bytes}"
|
||||
);
|
||||
assert_eq!(
|
||||
derived.resolved_max_bytes_for_effective_memory(effective_memory),
|
||||
Err(ObjectDataCacheConfigError::MaxEntryBytesExceedsCapacity),
|
||||
"derived max_entry_bytes={max_entry_bytes}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derived_max_bytes_clamps_to_v3_safe_cap() {
|
||||
let one_tib = 1024_u64 * 1024 * 1024 * 1024;
|
||||
|
||||
@@ -13,11 +13,26 @@
|
||||
// limitations under the License.
|
||||
|
||||
use bytes::Bytes;
|
||||
use std::cmp;
|
||||
|
||||
use crate::index::ObjectDataCacheGeneration;
|
||||
use crate::key::ObjectDataCacheKey;
|
||||
|
||||
const ENTRY_OVERHEAD_BYTES: usize = 64;
|
||||
const ENTRY_OVERHEAD_BYTES: u64 = 64;
|
||||
|
||||
/// Estimates the weighted capacity charged for a key and planned body.
|
||||
pub(crate) fn projected_weight(key: &ObjectDataCacheKey, body_bytes: u64) -> u64 {
|
||||
let key_bytes = key
|
||||
.bucket
|
||||
.len()
|
||||
.saturating_add(key.object.len())
|
||||
.saturating_add(key.version_id.len())
|
||||
.saturating_add(key.etag.len());
|
||||
|
||||
u64::try_from(key_bytes)
|
||||
.unwrap_or(u64::MAX)
|
||||
.saturating_add(body_bytes)
|
||||
.saturating_add(ENTRY_OVERHEAD_BYTES)
|
||||
}
|
||||
|
||||
/// Cached object body entry.
|
||||
///
|
||||
@@ -29,12 +44,17 @@ const ENTRY_OVERHEAD_BYTES: usize = 64;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjectDataCacheEntry {
|
||||
bytes: Bytes,
|
||||
generation: ObjectDataCacheGeneration,
|
||||
}
|
||||
|
||||
impl ObjectDataCacheEntry {
|
||||
/// Creates a new cached entry.
|
||||
pub fn new(bytes: Bytes) -> Self {
|
||||
Self { bytes }
|
||||
Self { bytes, generation: 0 }
|
||||
}
|
||||
|
||||
pub(crate) fn with_generation(bytes: Bytes, generation: ObjectDataCacheGeneration) -> Self {
|
||||
Self { bytes, generation }
|
||||
}
|
||||
|
||||
/// Returns a clone of the cached body bytes.
|
||||
@@ -42,14 +62,14 @@ impl ObjectDataCacheEntry {
|
||||
self.bytes.clone()
|
||||
}
|
||||
|
||||
pub(crate) const fn generation(&self) -> ObjectDataCacheGeneration {
|
||||
self.generation
|
||||
}
|
||||
|
||||
/// Returns the estimated weighted size for capacity accounting.
|
||||
pub fn estimated_weight(&self, key: &ObjectDataCacheKey) -> u32 {
|
||||
let key_bytes = key.bucket.len() + key.object.len() + key.version_id.len() + key.etag.len();
|
||||
let body_bytes = self.bytes.len();
|
||||
let total = key_bytes.saturating_add(body_bytes).saturating_add(ENTRY_OVERHEAD_BYTES);
|
||||
let clamped = cmp::min(total, u32::MAX as usize);
|
||||
|
||||
u32::try_from(clamped).unwrap_or(u32::MAX)
|
||||
let body_bytes = u64::try_from(self.bytes.len()).unwrap_or(u64::MAX);
|
||||
u32::try_from(projected_weight(key, body_bytes)).unwrap_or(u32::MAX)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,13 +15,24 @@
|
||||
use crate::key::ObjectDataCacheKey;
|
||||
use crate::starshard_index::StarshardIdentityIndex;
|
||||
|
||||
/// Per-entry generation token used to tell a freshly refilled body apart from a
|
||||
/// superseded one under the same key. The token is the cache entry's `Arc`
|
||||
/// pointer captured at fill time: the eviction listener receives the evicted
|
||||
/// value and can compare its pointer against the token currently tracked, so a
|
||||
/// deferred or inline eviction of an old generation cannot remove the index key
|
||||
/// registered for the current generation.
|
||||
/// Legacy public identity-index token type.
|
||||
///
|
||||
/// The backend uses an internal monotonic `u64` generation instead; keeping this
|
||||
/// alias preserves the existing public `StarshardIdentityIndex` method shapes.
|
||||
pub(crate) type ObjectDataCacheKeyToken = usize;
|
||||
pub(crate) type ObjectDataCacheGeneration = u64;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct ObjectDataCacheEvictedGeneration {
|
||||
pub(crate) key: ObjectDataCacheKey,
|
||||
pub(crate) generation: ObjectDataCacheGeneration,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum ObjectDataCacheGenerationalInsertResult {
|
||||
Inserted { evicted: Vec<ObjectDataCacheEvictedGeneration> },
|
||||
Duplicate,
|
||||
}
|
||||
|
||||
/// Result of inserting a cache key into the identity index.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -41,7 +52,7 @@ pub enum ObjectDataCacheIndexInsertResult {
|
||||
#[derive(Debug, Clone)]
|
||||
struct TrackedKey {
|
||||
key: ObjectDataCacheKey,
|
||||
token: ObjectDataCacheKeyToken,
|
||||
generation: ObjectDataCacheGeneration,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
@@ -50,37 +61,41 @@ pub(crate) struct ObjectDataCacheKeySet {
|
||||
}
|
||||
|
||||
impl ObjectDataCacheKeySet {
|
||||
pub(crate) fn insert(
|
||||
pub(crate) fn insert_generation(
|
||||
&mut self,
|
||||
key: ObjectDataCacheKey,
|
||||
token: ObjectDataCacheKeyToken,
|
||||
generation: ObjectDataCacheGeneration,
|
||||
max_keys: usize,
|
||||
) -> ObjectDataCacheIndexInsertResult {
|
||||
) -> ObjectDataCacheGenerationalInsertResult {
|
||||
if let Some(existing) = self.keys.iter_mut().find(|existing| existing.key == key) {
|
||||
// Refresh the generation token so a later eviction of the superseded
|
||||
// Refresh the generation so a later eviction of the superseded
|
||||
// body cannot remove the key registered for this new body.
|
||||
existing.token = token;
|
||||
return ObjectDataCacheIndexInsertResult::Duplicate;
|
||||
existing.generation = generation;
|
||||
return ObjectDataCacheGenerationalInsertResult::Duplicate;
|
||||
}
|
||||
|
||||
// Bounded eviction: the Vec preserves insertion order, so evicting from
|
||||
// the front drops the oldest keys and keeps hot (recently filled) ones,
|
||||
// rather than clearing the whole identity and rejecting the new key.
|
||||
let mut evicted_keys = Vec::new();
|
||||
let mut evicted = Vec::new();
|
||||
while self.keys.len() >= max_keys {
|
||||
evicted_keys.push(self.keys.remove(0).key);
|
||||
let tracked = self.keys.remove(0);
|
||||
evicted.push(ObjectDataCacheEvictedGeneration {
|
||||
key: tracked.key,
|
||||
generation: tracked.generation,
|
||||
});
|
||||
}
|
||||
|
||||
self.keys.push(TrackedKey { key, token });
|
||||
ObjectDataCacheIndexInsertResult::Inserted { evicted_keys }
|
||||
self.keys.push(TrackedKey { key, generation });
|
||||
ObjectDataCacheGenerationalInsertResult::Inserted { evicted }
|
||||
}
|
||||
|
||||
/// Removes the key only when its tracked generation token matches, so an
|
||||
/// eviction notification for an old generation leaves a refreshed key intact.
|
||||
pub(crate) fn remove_evicted_key(&mut self, key: &ObjectDataCacheKey, token: ObjectDataCacheKeyToken) -> bool {
|
||||
pub(crate) fn remove_generation(&mut self, key: &ObjectDataCacheKey, generation: ObjectDataCacheGeneration) -> bool {
|
||||
let original_len = self.keys.len();
|
||||
self.keys
|
||||
.retain(|existing| !(existing.key == *key && existing.token == token));
|
||||
.retain(|existing| !(existing.key == *key && existing.generation == generation));
|
||||
original_len != self.keys.len()
|
||||
}
|
||||
|
||||
@@ -99,6 +114,12 @@ impl ObjectDataCacheKeySet {
|
||||
self.keys.iter().any(|existing| &existing.key == key)
|
||||
}
|
||||
|
||||
pub(crate) fn contains_generation(&self, key: &ObjectDataCacheKey, generation: ObjectDataCacheGeneration) -> bool {
|
||||
self.keys
|
||||
.iter()
|
||||
.any(|existing| &existing.key == key && existing.generation == generation)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.keys.len()
|
||||
@@ -114,7 +135,7 @@ pub type ObjectDataCacheIdentityIndex = StarshardIdentityIndex;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ObjectDataCacheIndexInsertResult, ObjectDataCacheKeySet};
|
||||
use super::{ObjectDataCacheGenerationalInsertResult, ObjectDataCacheKeySet};
|
||||
use crate::key::{ObjectDataCacheBodyVariant, ObjectDataCacheKey};
|
||||
|
||||
fn make_key(id: &str) -> ObjectDataCacheKey {
|
||||
@@ -126,16 +147,11 @@ mod tests {
|
||||
let mut set = ObjectDataCacheKeySet::default();
|
||||
let key = make_key("v1");
|
||||
|
||||
let first = set.insert(key.clone(), 1, 4);
|
||||
let second = set.insert(key, 2, 4);
|
||||
let first = set.insert_generation(key.clone(), 1, 4);
|
||||
let second = set.insert_generation(key, 2, 4);
|
||||
|
||||
assert_eq!(
|
||||
first,
|
||||
ObjectDataCacheIndexInsertResult::Inserted {
|
||||
evicted_keys: Vec::new()
|
||||
}
|
||||
);
|
||||
assert_eq!(second, ObjectDataCacheIndexInsertResult::Duplicate);
|
||||
assert_eq!(first, ObjectDataCacheGenerationalInsertResult::Inserted { evicted: Vec::new() });
|
||||
assert_eq!(second, ObjectDataCacheGenerationalInsertResult::Duplicate);
|
||||
assert_eq!(set.len(), 1);
|
||||
}
|
||||
|
||||
@@ -145,12 +161,13 @@ mod tests {
|
||||
let key_a = make_key("v1");
|
||||
let key_b = make_key("v2");
|
||||
|
||||
let _ = set.insert(key_a.clone(), 1, 1);
|
||||
let result = set.insert(key_b.clone(), 2, 1);
|
||||
let _ = set.insert_generation(key_a.clone(), 1, 1);
|
||||
let result = set.insert_generation(key_b.clone(), 2, 1);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
ObjectDataCacheIndexInsertResult::Inserted { evicted_keys } if evicted_keys == vec![key_a]
|
||||
ObjectDataCacheGenerationalInsertResult::Inserted { evicted }
|
||||
if evicted.len() == 1 && evicted[0].key == key_a && evicted[0].generation == 1
|
||||
));
|
||||
// The new key replaces the evicted one instead of the identity being cleared.
|
||||
assert!(set.contains(&key_b));
|
||||
@@ -162,14 +179,14 @@ mod tests {
|
||||
let mut set = ObjectDataCacheKeySet::default();
|
||||
let key = make_key("v1");
|
||||
|
||||
let _ = set.insert(key.clone(), 1, 4);
|
||||
let _ = set.insert_generation(key.clone(), 1, 4);
|
||||
|
||||
// A stale eviction notification carrying the old token must not remove the key.
|
||||
assert!(!set.remove_evicted_key(&key, 2));
|
||||
assert!(!set.remove_generation(&key, 2));
|
||||
assert!(set.contains(&key));
|
||||
|
||||
// The matching token removes the key.
|
||||
assert!(set.remove_evicted_key(&key, 1));
|
||||
assert!(set.remove_generation(&key, 1));
|
||||
assert!(!set.contains(&key));
|
||||
}
|
||||
|
||||
@@ -178,11 +195,11 @@ mod tests {
|
||||
let mut set = ObjectDataCacheKeySet::default();
|
||||
let key = make_key("v1");
|
||||
|
||||
let _ = set.insert(key.clone(), 1, 4);
|
||||
let _ = set.insert(key.clone(), 2, 4);
|
||||
let _ = set.insert_generation(key.clone(), 1, 4);
|
||||
let _ = set.insert_generation(key.clone(), 2, 4);
|
||||
|
||||
// After the refresh the old token no longer matches.
|
||||
assert!(!set.remove_evicted_key(&key, 1));
|
||||
assert!(set.remove_evicted_key(&key, 2));
|
||||
assert!(!set.remove_generation(&key, 1));
|
||||
assert!(set.remove_generation(&key, 2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,8 +57,9 @@ pub mod starshard_index;
|
||||
pub mod stats;
|
||||
|
||||
pub use cache::{
|
||||
ObjectDataCache, ObjectDataCacheFillResult, ObjectDataCacheGetPlan, ObjectDataCacheGetRequest,
|
||||
ObjectDataCacheInvalidationReason, ObjectDataCacheInvalidationResult, ObjectDataCacheLookup,
|
||||
ObjectDataCache, ObjectDataCacheBodyReservation, ObjectDataCacheFillResult, ObjectDataCacheGetPlan,
|
||||
ObjectDataCacheGetRequest, ObjectDataCacheInvalidationReason, ObjectDataCacheInvalidationResult, ObjectDataCacheLookup,
|
||||
ObjectDataCacheReservedBody,
|
||||
};
|
||||
pub use config::{ObjectDataCacheConfig, ObjectDataCacheMode};
|
||||
pub use error::ObjectDataCacheConfigError;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,7 @@ fn lock_fills(fills: &FillSet) -> std::sync::MutexGuard<'_, HashSet<ObjectDataCa
|
||||
/// reports [`Busy`](ObjectDataCacheSingleflightAcquire::Busy) to everyone else.
|
||||
#[derive(Debug)]
|
||||
pub struct ObjectDataCacheSingleflight {
|
||||
fills: FillSet,
|
||||
fills: Arc<FillSet>,
|
||||
stats: Arc<ObjectDataCacheStats>,
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ impl ObjectDataCacheSingleflight {
|
||||
/// Creates a new singleflight controller.
|
||||
pub fn new(stats: Arc<ObjectDataCacheStats>) -> Self {
|
||||
Self {
|
||||
fills: Mutex::new(HashSet::new()),
|
||||
fills: Arc::new(Mutex::new(HashSet::new())),
|
||||
stats,
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ impl ObjectDataCacheSingleflight {
|
||||
/// [`Busy`](ObjectDataCacheSingleflightAcquire::Busy). The caller already
|
||||
/// owns the body, so a `Busy` outcome skips the redundant fill rather than
|
||||
/// waiting for another request's leader to finish.
|
||||
pub fn try_acquire(&self, key: ObjectDataCacheKey) -> ObjectDataCacheSingleflightAcquire<'_> {
|
||||
pub fn try_acquire(&self, key: ObjectDataCacheKey) -> ObjectDataCacheSingleflightAcquire<'static> {
|
||||
// Keep the critical section to the map mutation only; emit metrics after
|
||||
// dropping the guard so the recorder round-trip never serializes fills.
|
||||
let inflight_len = {
|
||||
@@ -75,9 +75,10 @@ impl ObjectDataCacheSingleflight {
|
||||
set_inflight_fills(&self.stats, "moka", len);
|
||||
ObjectDataCacheSingleflightAcquire::Leader(ObjectDataCacheSingleflightLeader {
|
||||
key,
|
||||
fills: &self.fills,
|
||||
fills: Arc::clone(&self.fills),
|
||||
stats: Arc::clone(&self.stats),
|
||||
finished: false,
|
||||
lifetime: std::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -104,12 +105,13 @@ pub enum ObjectDataCacheSingleflightAcquire<'a> {
|
||||
/// Leader handle for a singleflight fill operation.
|
||||
pub struct ObjectDataCacheSingleflightLeader<'a> {
|
||||
key: ObjectDataCacheKey,
|
||||
fills: &'a FillSet,
|
||||
fills: Arc<FillSet>,
|
||||
stats: Arc<ObjectDataCacheStats>,
|
||||
finished: bool,
|
||||
lifetime: std::marker::PhantomData<&'a ()>,
|
||||
}
|
||||
|
||||
impl<'a> ObjectDataCacheSingleflightLeader<'a> {
|
||||
impl ObjectDataCacheSingleflightLeader<'_> {
|
||||
/// Completes the leader operation and releases the key.
|
||||
pub fn finish(mut self, result: ObjectDataCacheFillResult) -> ObjectDataCacheFillResult {
|
||||
self.remove_entry();
|
||||
@@ -121,7 +123,7 @@ impl<'a> ObjectDataCacheSingleflightLeader<'a> {
|
||||
// Capture the length under the guard, then emit the gauge after dropping
|
||||
// it so the recorder round-trip stays out of the critical section.
|
||||
let len = {
|
||||
let mut fills = lock_fills(self.fills);
|
||||
let mut fills = lock_fills(&self.fills);
|
||||
fills.remove(&self.key);
|
||||
fills.len()
|
||||
};
|
||||
@@ -129,7 +131,7 @@ impl<'a> ObjectDataCacheSingleflightLeader<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Drop for ObjectDataCacheSingleflightLeader<'a> {
|
||||
impl Drop for ObjectDataCacheSingleflightLeader<'_> {
|
||||
fn drop(&mut self) {
|
||||
// A leader dropped without finish() was cancelled mid-fill (e.g. the
|
||||
// fill task was aborted). Release the key so a later fill can become the
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user