1#[must_use]
14pub fn select_retention_set(gains: &mut [u32], n: u32, k: u32) -> Vec<u32> {
15 match try_select_retention_set(gains, n, k) {
16 Ok(picked) => picked,
17 Err(error) => {
18 tracing::error!("{error}");
19 Vec::new()
20 }
21 }
22}
23
24pub fn try_select_retention_set(
26 gains: &mut [u32],
27 n: u32,
28 k: u32,
29) -> Result<Vec<u32>, CacheEvictionAllocationError> {
30 let effective_n = effective_len(gains, n);
31 let mut picked = Vec::new();
32 reserve_picked(&mut picked, effective_n)?;
33 select_retention_set_into(gains, n, k, &mut picked);
34 Ok(picked)
35}
36
37pub fn select_retention_set_into(gains: &mut [u32], n: u32, k: u32, picked: &mut Vec<u32>) {
39 let effective_n = effective_len(gains, n);
40 let keep_limit = (k as usize).min(effective_n);
41 picked.clear();
42 picked.resize(effective_n, 0);
43 let mut keep_count = 0usize;
44 while keep_count < keep_limit {
45 let Some(winner) = argmax_unpicked(&gains[..effective_n], picked) else {
46 break;
47 };
48 picked[winner] = 1;
49 gains[winner] = 0;
50 keep_count += 1;
51 }
52}
53
54pub fn try_select_retention_set_into(
56 gains: &mut [u32],
57 n: u32,
58 k: u32,
59 picked: &mut Vec<u32>,
60) -> Result<(), CacheEvictionAllocationError> {
61 let effective_n = effective_len(gains, n);
62 reserve_picked(picked, effective_n)?;
63 select_retention_set_into(gains, n, k, picked);
64 Ok(())
65}
66
67#[derive(Clone, Debug, Eq, PartialEq)]
69pub struct CacheEvictionAllocationError {
70 pub requested: usize,
72 pub message: String,
74}
75
76impl std::fmt::Display for CacheEvictionAllocationError {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 write!(
79 f,
80 "cache eviction failed to reserve {} retention entries: {}. Fix: shard cache eviction or lower cache soft caps before eviction planning.",
81 self.requested, self.message
82 )
83 }
84}
85
86impl std::error::Error for CacheEvictionAllocationError {}
87
88pub fn record_eviction(dropped_fraction: f64) {
90 let dropped_fraction = if dropped_fraction.is_finite() {
91 dropped_fraction.clamp(0.0, 1.0)
92 } else {
93 0.0
94 };
95 tracing::trace!(
96 target: "vyre.driver.eviction",
97 dropped_fraction,
98 "cache eviction decision",
99 );
100}
101
102pub fn record_eviction_counts(dropped_entries: usize, total_entries: usize) {
104 let dropped_basis_points = eviction_basis_points(dropped_entries, total_entries);
105 tracing::trace!(
106 target: "vyre.driver.eviction",
107 dropped_entries,
108 total_entries,
109 dropped_basis_points,
110 dropped_fraction = f64::from(dropped_basis_points) / 10_000.0,
111 "cache eviction decision",
112 );
113}
114
115#[must_use]
117pub fn eviction_basis_points(dropped_entries: usize, total_entries: usize) -> u32 {
118 if total_entries == 0 {
119 return 0;
120 }
121 let bounded_dropped = dropped_entries.min(total_entries);
122 let dropped = u64::try_from(bounded_dropped).unwrap_or(u64::MAX);
123 let total = u64::try_from(total_entries).unwrap_or(u64::MAX);
124 crate::numeric::ratio_basis_points_u64(
125 dropped,
126 total,
127 0,
128 "cache eviction dropped entries",
129 "driver",
130 )
131 .min(10_000)
132}
133
134fn effective_len(gains: &[u32], n: u32) -> usize {
135 gains.len().min(n as usize)
136}
137
138fn reserve_picked(
139 picked: &mut Vec<u32>,
140 effective_n: usize,
141) -> Result<(), CacheEvictionAllocationError> {
142 crate::allocation::try_reserve_vec_to_capacity(picked, effective_n).map_err(|error| {
143 CacheEvictionAllocationError {
144 requested: effective_n,
145 message: error.to_string(),
146 }
147 })
148}
149
150fn argmax_unpicked(gains: &[u32], picked: &[u32]) -> Option<usize> {
151 let mut best: Option<(usize, u32)> = None;
152 for (idx, gain) in gains.iter().copied().enumerate() {
153 if picked.get(idx).copied().unwrap_or(0) != 0 || gain == 0 {
154 continue;
155 }
156 match best {
157 Some((_, current)) if gain <= current => {}
158 _ => best = Some((idx, gain)),
159 }
160 }
161 best.map(|(idx, _)| idx)
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn retains_top_k_gains() {
170 let mut gains = vec![3, 10, 2, 8, 1];
171 let picked = select_retention_set(&mut gains, 5, 2);
172 assert_eq!(picked, vec![0, 1, 0, 1, 0]);
173 }
174
175 #[test]
176 fn zero_k_evicts_all() {
177 let mut gains = vec![3, 10, 2];
178 let picked = select_retention_set(&mut gains, 3, 0);
179 assert_eq!(picked, vec![0, 0, 0]);
180 }
181
182 #[test]
183 fn k_equal_n_keeps_positive_gain_entries() {
184 let mut gains = vec![3, 0, 2];
185 let picked = select_retention_set(&mut gains, 3, 3);
186 assert_eq!(picked, vec![1, 0, 1]);
187 }
188
189 #[test]
190 fn into_reuses_storage() {
191 let mut gains = vec![1, 9, 4];
192 let mut picked = Vec::with_capacity(8);
193 let ptr = picked.as_ptr();
194 select_retention_set_into(&mut gains, 3, 2, &mut picked);
195 assert_eq!(picked, vec![0, 1, 1]);
196 assert_eq!(picked.as_ptr(), ptr);
197 }
198
199 #[test]
200 fn try_into_reuses_storage() {
201 let mut gains = vec![1, 9, 4];
202 let mut picked = Vec::with_capacity(8);
203 let ptr = picked.as_ptr();
204 try_select_retention_set_into(&mut gains, 3, 2, &mut picked)
205 .expect("Fix: retention scratch should be reusable");
206 assert_eq!(picked, vec![0, 1, 1]);
207 assert_eq!(picked.as_ptr(), ptr);
208 }
209
210 #[test]
211 fn invalid_sizing_is_clamped_not_panicked() {
212 let mut gains = vec![5, 1];
213 let picked = select_retention_set(&mut gains, 99, 99);
214 assert_eq!(picked, vec![1, 1]);
215 }
216
217 #[test]
218 fn eviction_basis_points_are_exact_and_bounded() {
219 assert_eq!(eviction_basis_points(0, 0), 0);
220 assert_eq!(eviction_basis_points(1, 2), 5_000);
221 assert_eq!(eviction_basis_points(476, 512), 9_296);
222 assert_eq!(eviction_basis_points(9, 3), 10_000);
223 assert_eq!(eviction_basis_points(usize::MAX, usize::MAX), 10_000);
224 }
225
226 #[test]
227 fn eviction_recording_accepts_hostile_ratios() {
228 record_eviction(f64::NAN);
229 record_eviction(f64::INFINITY);
230 record_eviction(f64::NEG_INFINITY);
231 record_eviction_counts(usize::MAX, 1);
232 }
233
234 #[test]
235 fn release_eviction_selector_exposes_fallible_allocation_path() {
236 let source = include_str!("cache_eviction.rs");
237 assert!(
238 source.contains("pub fn try_select_retention_set")
239 && source.contains("pub fn try_select_retention_set_into")
240 && source.contains("try_reserve_vec_to_capacity"),
241 "Fix: release cache eviction callers need a fallible selector path instead of infallible Vec allocation."
242 );
243 assert!(
244 !source.contains(concat!("Vec::with_capacity", "(effective_len")),
245 "Fix: cache eviction selector must not allocate retention vectors infallibly on release paths."
246 );
247 }
248}