#[must_use]
pub fn select_retention_set(gains: &mut [u32], n: u32, k: u32) -> Vec<u32> {
match try_select_retention_set(gains, n, k) {
Ok(picked) => picked,
Err(error) => {
tracing::error!("{error}");
Vec::new()
}
}
}
pub fn try_select_retention_set(
gains: &mut [u32],
n: u32,
k: u32,
) -> Result<Vec<u32>, CacheEvictionAllocationError> {
let effective_n = effective_len(gains, n);
let mut picked = Vec::new();
reserve_picked(&mut picked, effective_n)?;
select_retention_set_into(gains, n, k, &mut picked);
Ok(picked)
}
pub fn select_retention_set_into(gains: &mut [u32], n: u32, k: u32, picked: &mut Vec<u32>) {
let effective_n = effective_len(gains, n);
let keep_limit = (k as usize).min(effective_n);
picked.clear();
picked.resize(effective_n, 0);
let mut keep_count = 0usize;
while keep_count < keep_limit {
let Some(winner) = argmax_unpicked(&gains[..effective_n], picked) else {
break;
};
picked[winner] = 1;
gains[winner] = 0;
keep_count += 1;
}
}
pub fn try_select_retention_set_into(
gains: &mut [u32],
n: u32,
k: u32,
picked: &mut Vec<u32>,
) -> Result<(), CacheEvictionAllocationError> {
let effective_n = effective_len(gains, n);
reserve_picked(picked, effective_n)?;
select_retention_set_into(gains, n, k, picked);
Ok(())
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CacheEvictionAllocationError {
pub requested: usize,
pub message: String,
}
impl std::fmt::Display for CacheEvictionAllocationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"cache eviction failed to reserve {} retention entries: {}. Fix: shard cache eviction or lower cache soft caps before eviction planning.",
self.requested, self.message
)
}
}
impl std::error::Error for CacheEvictionAllocationError {}
pub fn record_eviction(dropped_fraction: f64) {
let dropped_fraction = if dropped_fraction.is_finite() {
dropped_fraction.clamp(0.0, 1.0)
} else {
0.0
};
tracing::trace!(
target: "vyre.driver.eviction",
dropped_fraction,
"cache eviction decision",
);
}
pub fn record_eviction_counts(dropped_entries: usize, total_entries: usize) {
let dropped_basis_points = eviction_basis_points(dropped_entries, total_entries);
tracing::trace!(
target: "vyre.driver.eviction",
dropped_entries,
total_entries,
dropped_basis_points,
dropped_fraction = f64::from(dropped_basis_points) / 10_000.0,
"cache eviction decision",
);
}
#[must_use]
pub fn eviction_basis_points(dropped_entries: usize, total_entries: usize) -> u32 {
if total_entries == 0 {
return 0;
}
let bounded_dropped = dropped_entries.min(total_entries);
let dropped = u64::try_from(bounded_dropped).unwrap_or(u64::MAX);
let total = u64::try_from(total_entries).unwrap_or(u64::MAX);
crate::numeric::ratio_basis_points_u64(
dropped,
total,
0,
"cache eviction dropped entries",
"driver",
)
.min(10_000)
}
fn effective_len(gains: &[u32], n: u32) -> usize {
gains.len().min(n as usize)
}
fn reserve_picked(
picked: &mut Vec<u32>,
effective_n: usize,
) -> Result<(), CacheEvictionAllocationError> {
crate::allocation::try_reserve_vec_to_capacity(picked, effective_n).map_err(|error| {
CacheEvictionAllocationError {
requested: effective_n,
message: error.to_string(),
}
})
}
fn argmax_unpicked(gains: &[u32], picked: &[u32]) -> Option<usize> {
let mut best: Option<(usize, u32)> = None;
for (idx, gain) in gains.iter().copied().enumerate() {
if picked.get(idx).copied().unwrap_or(0) != 0 || gain == 0 {
continue;
}
match best {
Some((_, current)) if gain <= current => {}
_ => best = Some((idx, gain)),
}
}
best.map(|(idx, _)| idx)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn retains_top_k_gains() {
let mut gains = vec![3, 10, 2, 8, 1];
let picked = select_retention_set(&mut gains, 5, 2);
assert_eq!(picked, vec![0, 1, 0, 1, 0]);
}
#[test]
fn zero_k_evicts_all() {
let mut gains = vec![3, 10, 2];
let picked = select_retention_set(&mut gains, 3, 0);
assert_eq!(picked, vec![0, 0, 0]);
}
#[test]
fn k_equal_n_keeps_positive_gain_entries() {
let mut gains = vec![3, 0, 2];
let picked = select_retention_set(&mut gains, 3, 3);
assert_eq!(picked, vec![1, 0, 1]);
}
#[test]
fn into_reuses_storage() {
let mut gains = vec![1, 9, 4];
let mut picked = Vec::with_capacity(8);
let ptr = picked.as_ptr();
select_retention_set_into(&mut gains, 3, 2, &mut picked);
assert_eq!(picked, vec![0, 1, 1]);
assert_eq!(picked.as_ptr(), ptr);
}
#[test]
fn try_into_reuses_storage() {
let mut gains = vec![1, 9, 4];
let mut picked = Vec::with_capacity(8);
let ptr = picked.as_ptr();
try_select_retention_set_into(&mut gains, 3, 2, &mut picked)
.expect("Fix: retention scratch should be reusable");
assert_eq!(picked, vec![0, 1, 1]);
assert_eq!(picked.as_ptr(), ptr);
}
#[test]
fn invalid_sizing_is_clamped_not_panicked() {
let mut gains = vec![5, 1];
let picked = select_retention_set(&mut gains, 99, 99);
assert_eq!(picked, vec![1, 1]);
}
#[test]
fn eviction_basis_points_are_exact_and_bounded() {
assert_eq!(eviction_basis_points(0, 0), 0);
assert_eq!(eviction_basis_points(1, 2), 5_000);
assert_eq!(eviction_basis_points(476, 512), 9_296);
assert_eq!(eviction_basis_points(9, 3), 10_000);
assert_eq!(eviction_basis_points(usize::MAX, usize::MAX), 10_000);
}
#[test]
fn eviction_recording_accepts_hostile_ratios() {
record_eviction(f64::NAN);
record_eviction(f64::INFINITY);
record_eviction(f64::NEG_INFINITY);
record_eviction_counts(usize::MAX, 1);
}
#[test]
fn release_eviction_selector_exposes_fallible_allocation_path() {
let source = include_str!("cache_eviction.rs");
assert!(
source.contains("pub fn try_select_retention_set")
&& source.contains("pub fn try_select_retention_set_into")
&& source.contains("try_reserve_vec_to_capacity"),
"Fix: release cache eviction callers need a fallible selector path instead of infallible Vec allocation."
);
assert!(
!source.contains(concat!("Vec::with_capacity", "(effective_len")),
"Fix: cache eviction selector must not allocate retention vectors infallibly on release paths."
);
}
}