1use std::collections::{BTreeMap, VecDeque};
8use std::sync::Mutex;
9
10use rustev_contract::canonical::{record_canonical_bytes, tagged_digest};
11use rustev_contract::ids::{ArtifactId, DescriptorId, PlanId};
12use rustev_contract::optimization::{MemoryCachePolicy, OptimizationPolicy};
13use rustev_contract::output::RawOutput;
14
15use crate::clock::Instant;
16
17const KEY_TAG: &str = "rustev.optimization-key/1";
18const ENTRY_TAG: &str = "rustev.optimization-entry/1";
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub(crate) struct RequestKey {
22 pub id: String,
23 pub material: Vec<u8>,
24 pub scope_id: String,
25 pub namespace: String,
26}
27
28pub(crate) struct KeyParts<'a> {
41 pub plan_id: &'a PlanId,
42 pub policy: &'a OptimizationPolicy,
43 pub principal: &'a [u8],
44 pub backend_artifact: &'a ArtifactId,
45 pub backend_descriptor: &'a DescriptorId,
46 pub step: &'a str,
47 pub instance: &'a [String],
48 pub projection: &'a [u8],
49}
50
51fn part(out: &mut Vec<u8>, bytes: &[u8]) {
52 out.extend_from_slice(&(bytes.len() as u64).to_be_bytes());
53 out.extend_from_slice(bytes);
54}
55
56pub(crate) fn request_key(p: KeyParts<'_>) -> RequestKey {
57 let mut material = Vec::new();
58 part(&mut material, p.plan_id.as_str().as_bytes());
59 part(&mut material, &p.policy.key_schema_version.to_be_bytes());
60 part(
61 &mut material,
62 &p.policy.cache_namespace_version.to_be_bytes(),
63 );
64 part(&mut material, env!("CARGO_PKG_VERSION").as_bytes());
65 part(&mut material, p.principal);
66 part(&mut material, p.backend_artifact.as_str().as_bytes());
67 part(&mut material, p.backend_descriptor.as_str().as_bytes());
68 part(&mut material, p.step.as_bytes());
69 part(&mut material, &(p.instance.len() as u64).to_be_bytes());
70 for member in p.instance {
71 part(&mut material, member.as_bytes());
72 }
73 part(&mut material, p.projection);
74 let scope_id = scope_id(p.principal);
75 let namespace = namespace(p.plan_id, p.policy);
76 RequestKey {
77 id: tagged_digest(KEY_TAG, &material),
78 material,
79 scope_id,
80 namespace,
81 }
82}
83
84pub(crate) fn scope_id(principal: &[u8]) -> String {
85 tagged_digest("rustev.optimization-scope/1", principal)
86}
87
88pub(crate) fn namespace(plan_id: &PlanId, policy: &OptimizationPolicy) -> String {
89 format!(
90 "{}/{}/{}",
91 plan_id, policy.key_schema_version, policy.cache_namespace_version
92 )
93}
94
95#[derive(Debug, Clone, PartialEq)]
96pub(crate) struct CachedOutput {
97 pub output: RawOutput,
98 pub source_attempt_id: String,
99 pub target: u32,
100 pub entry_id: String,
101 pub age_ms: u64,
102 pub generation: u64,
103}
104
105pub(crate) struct CacheWrite {
106 pub output: RawOutput,
107 pub source_attempt_id: String,
108 pub target: u32,
109 pub captured_generation: u64,
110 pub now: Instant,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub(crate) struct CacheAdmission {
115 pub entry_id: String,
116 pub evicted_entries: u64,
117 pub evicted_bytes: u64,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub(crate) enum CacheMiss {
123 Absent,
124 Expired,
125 Collision,
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub(crate) enum CacheRefusal {
133 Uncanonical,
135 EntryTooLarge,
137 StaleGeneration,
139 Collision,
141}
142
143#[derive(Debug, Clone)]
144enum CachedValue {
145 Semantic(RawOutput),
146 Page(Vec<u8>),
147}
148
149struct StoredWrite {
150 output: CachedValue,
151 source_attempt_id: String,
152 target: u32,
153 captured_generation: u64,
154 now: Instant,
155}
156
157pub(crate) struct PageWrite {
158 pub bytes: Vec<u8>,
159 pub attempt: String,
160 pub generation: u64,
161 pub now: Instant,
162}
163
164#[derive(Debug, Clone)]
165struct Entry {
166 key: RequestKey,
167 output: CachedValue,
168 source_attempt_id: String,
169 target: u32,
170 entry_id: String,
171 created: Instant,
172 expires: Instant,
173 generation: u64,
174 bytes: u64,
175 seq: u64,
178}
179
180type Slot = (String, u64);
181
182#[derive(Debug, Default)]
183struct NamespaceState {
184 generation: u64,
185 entries: BTreeMap<String, Entry>,
186 fifo: VecDeque<Slot>,
187 scope_fifo: BTreeMap<String, VecDeque<Slot>>,
188 scope_bytes: BTreeMap<String, u64>,
189 total_bytes: u64,
190 next_seq: u64,
191}
192
193impl NamespaceState {
194 fn live(&self, slot: &Slot) -> bool {
195 self.entries.get(&slot.0).is_some_and(|e| e.seq == slot.1)
196 }
197
198 fn remove(&mut self, id: &str) -> Option<Entry> {
199 let entry = self.entries.remove(id)?;
200 self.total_bytes = self.total_bytes.saturating_sub(entry.bytes);
201 if let Some(bytes) = self.scope_bytes.get_mut(&entry.key.scope_id) {
202 *bytes = bytes.saturating_sub(entry.bytes);
203 if *bytes == 0 {
204 self.scope_bytes.remove(&entry.key.scope_id);
205 }
206 }
207 if self.fifo.len() > 2 * self.entries.len() + 16 {
210 let entries = &self.entries;
211 self.fifo
212 .retain(|s| entries.get(&s.0).is_some_and(|e| e.seq == s.1));
213 }
214 if let Some(queue) = self.scope_fifo.get_mut(&entry.key.scope_id) {
215 let live = self.scope_bytes.contains_key(&entry.key.scope_id);
216 if !live {
217 self.scope_fifo.remove(&entry.key.scope_id);
218 } else if queue.len() > 16 {
219 let entries = &self.entries;
220 queue.retain(|s| entries.get(&s.0).is_some_and(|e| e.seq == s.1));
221 }
222 }
223 Some(entry)
224 }
225
226 fn evict_oldest(&mut self, scope: Option<&str>) -> Option<Entry> {
228 loop {
229 let slot = match scope {
230 Some(scope) => self.scope_fifo.get_mut(scope)?.pop_front()?,
231 None => self.fifo.pop_front()?,
232 };
233 if self.live(&slot) {
234 return self.remove(&slot.0);
235 }
236 }
237 }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq)]
242pub(crate) enum InvalidationSelector {
243 Namespace { namespace: String },
244 Scope { namespace: String, scope_id: String },
245}
246
247#[derive(Debug, Clone, PartialEq, Eq)]
248pub struct InvalidationResult {
249 pub generation: u64,
252 pub removed_entries: u64,
253 pub removed_bytes: u64,
254}
255
256#[derive(Debug, Default)]
257pub(crate) struct Optimizer {
258 namespaces: Mutex<BTreeMap<String, NamespaceState>>,
259}
260
261impl Optimizer {
262 fn lock(&self) -> std::sync::MutexGuard<'_, BTreeMap<String, NamespaceState>> {
263 self.namespaces.lock().unwrap_or_else(|e| e.into_inner())
264 }
265
266 pub(crate) fn generation(&self, namespace: &str) -> u64 {
267 self.lock().get(namespace).map_or(0, |ns| ns.generation)
268 }
269
270 fn lookup_entry(&self, key: &RequestKey, now: Instant) -> Result<Entry, CacheMiss> {
271 let mut namespaces = self.lock();
272 let Some(ns) = namespaces.get_mut(&key.namespace) else {
273 return Err(CacheMiss::Absent);
274 };
275 let Some(entry) = ns.entries.get(&key.id) else {
276 return Err(CacheMiss::Absent);
277 };
278 if entry.key.material != key.material {
279 return Err(CacheMiss::Collision);
280 }
281 if entry.expires <= now || entry.generation != ns.generation {
282 ns.remove(&key.id);
283 return Err(CacheMiss::Expired);
284 }
285 Ok(entry.clone())
286 }
287
288 pub(crate) fn lookup(&self, key: &RequestKey, now: Instant) -> Result<CachedOutput, CacheMiss> {
289 let e = self.lookup_entry(key, now)?;
290 let CachedValue::Semantic(output) = e.output else {
291 return Err(CacheMiss::Collision);
292 };
293 Ok(CachedOutput {
294 output,
295 source_attempt_id: e.source_attempt_id,
296 target: e.target,
297 entry_id: e.entry_id,
298 age_ms: now.saturating_sub(e.created),
299 generation: e.generation,
300 })
301 }
302
303 pub(crate) fn lookup_page(
304 &self,
305 key: &RequestKey,
306 now: Instant,
307 ) -> Result<(Vec<u8>, u64), CacheMiss> {
308 let e = self.lookup_entry(key, now)?;
309 let CachedValue::Page(bytes) = e.output else {
310 return Err(CacheMiss::Collision);
311 };
312 Ok((bytes, now.saturating_sub(e.created)))
313 }
314
315 pub(crate) fn insert_page(
316 &self,
317 policy: &MemoryCachePolicy,
318 key: RequestKey,
319 page: PageWrite,
320 ) -> Result<CacheAdmission, CacheRefusal> {
321 self.insert_value(
322 policy,
323 key,
324 StoredWrite {
325 source_attempt_id: page.attempt,
326 target: 0,
327 captured_generation: page.generation,
328 now: page.now,
329 output: CachedValue::Page(page.bytes),
330 },
331 )
332 }
333
334 pub(crate) fn insert(
335 &self,
336 policy: &MemoryCachePolicy,
337 key: RequestKey,
338 write: CacheWrite,
339 ) -> Result<CacheAdmission, CacheRefusal> {
340 self.insert_value(
341 policy,
342 key,
343 StoredWrite {
344 output: CachedValue::Semantic(write.output),
345 source_attempt_id: write.source_attempt_id,
346 target: write.target,
347 captured_generation: write.captured_generation,
348 now: write.now,
349 },
350 )
351 }
352
353 fn insert_value(
354 &self,
355 policy: &MemoryCachePolicy,
356 key: RequestKey,
357 write: StoredWrite,
358 ) -> Result<CacheAdmission, CacheRefusal> {
359 let output_bytes = match &write.output {
360 CachedValue::Semantic(output) => {
361 record_canonical_bytes(output).map_err(|_| CacheRefusal::Uncanonical)?
362 }
363 CachedValue::Page(bytes) => bytes.clone(),
364 };
365 let bytes = key
366 .material
367 .len()
368 .saturating_add(output_bytes.len())
369 .saturating_add(write.source_attempt_id.len()) as u64;
370 if bytes > policy.max_entry_bytes {
374 return Err(CacheRefusal::EntryTooLarge);
375 }
376 let mut namespaces = self.lock();
377 let ns = namespaces.entry(key.namespace.clone()).or_default();
378 if ns.generation != write.captured_generation {
379 return Err(CacheRefusal::StaleGeneration);
380 }
381 if ns
382 .entries
383 .get(&key.id)
384 .is_some_and(|entry| entry.key.material != key.material)
385 {
386 return Err(CacheRefusal::Collision);
387 }
388 ns.remove(&key.id);
390 let mut evicted_entries = 0_u64;
391 let mut evicted_bytes = 0_u64;
392 let mut evicted = |entry: Entry| {
393 evicted_entries += 1;
394 evicted_bytes = evicted_bytes.saturating_add(entry.bytes);
395 };
396 while ns
397 .scope_bytes
398 .get(&key.scope_id)
399 .copied()
400 .unwrap_or(0)
401 .saturating_add(bytes)
402 > policy.max_scope_bytes
403 {
404 let Some(entry) = ns.evict_oldest(Some(&key.scope_id)) else {
405 break;
406 };
407 evicted(entry);
408 }
409 while ns.entries.len() as u64 >= policy.max_entries
410 || ns.total_bytes.saturating_add(bytes) > policy.max_bytes
411 {
412 let Some(entry) = ns.evict_oldest(None) else {
413 break;
414 };
415 evicted(entry);
416 }
417 let mut entry_material = key.material.clone();
418 part(&mut entry_material, &output_bytes);
419 part(&mut entry_material, write.source_attempt_id.as_bytes());
420 let entry_id = tagged_digest(ENTRY_TAG, &entry_material);
421 let seq = ns.next_seq;
422 ns.next_seq += 1;
423 *ns.scope_bytes.entry(key.scope_id.clone()).or_default() += bytes;
424 ns.total_bytes += bytes;
425 ns.fifo.push_back((key.id.clone(), seq));
426 ns.scope_fifo
427 .entry(key.scope_id.clone())
428 .or_default()
429 .push_back((key.id.clone(), seq));
430 let generation = ns.generation;
431 ns.entries.insert(
432 key.id.clone(),
433 Entry {
434 key,
435 output: write.output,
436 source_attempt_id: write.source_attempt_id,
437 target: write.target,
438 entry_id: entry_id.clone(),
439 created: write.now,
440 expires: write.now.saturating_add(policy.ttl_ms),
441 generation,
442 bytes,
443 seq,
444 },
445 );
446 Ok(CacheAdmission {
447 entry_id,
448 evicted_entries,
449 evicted_bytes,
450 })
451 }
452
453 pub(crate) fn invalidate(&self, selector: &InvalidationSelector) -> InvalidationResult {
456 let namespace = match selector {
457 InvalidationSelector::Namespace { namespace }
458 | InvalidationSelector::Scope { namespace, .. } => namespace,
459 };
460 let mut namespaces = self.lock();
461 let ns = namespaces.entry(namespace.clone()).or_default();
462 ns.generation = ns.generation.saturating_add(1);
463 let ids: Vec<String> = ns
464 .entries
465 .iter()
466 .filter(|(_, entry)| match selector {
467 InvalidationSelector::Namespace { .. } => true,
468 InvalidationSelector::Scope { scope_id, .. } => &entry.key.scope_id == scope_id,
469 })
470 .map(|(id, _)| id.clone())
471 .collect();
472 let mut removed_bytes = 0;
473 for id in &ids {
474 if let Some(entry) = ns.remove(id) {
475 removed_bytes += entry.bytes;
476 }
477 }
478 InvalidationResult {
479 generation: ns.generation,
480 removed_entries: ids.len() as u64,
481 removed_bytes,
482 }
483 }
484
485 #[cfg(test)]
486 fn force_collision(&self, key: &RequestKey, other_material: Vec<u8>) {
487 let mut namespaces = self.lock();
488 if let Some(entry) = namespaces
489 .get_mut(&key.namespace)
490 .and_then(|ns| ns.entries.get_mut(&key.id))
491 {
492 entry.key.material = other_material;
493 }
494 }
495
496 #[cfg(test)]
497 fn total_bytes(&self, namespace: &str) -> u64 {
498 self.lock().get(namespace).map_or(0, |ns| ns.total_bytes)
499 }
500
501 #[cfg(test)]
502 fn queue_lengths(&self, namespace: &str) -> (usize, usize) {
503 let namespaces = self.lock();
504 let ns = &namespaces[namespace];
505 (
506 ns.fifo.len(),
507 ns.scope_fifo.values().map(VecDeque::len).sum(),
508 )
509 }
510}
511
512pub(crate) fn acquisition_key(
514 plan: &PlanId,
515 policy: &OptimizationPolicy,
516 principal: &[u8],
517 request: &rustev_core::evaluate::PendingAcquisition,
518) -> RequestKey {
519 let mut material = Vec::new();
520 part(&mut material, b"acquisition-page");
521 part(&mut material, &policy.key_schema_version.to_be_bytes());
522 part(&mut material, env!("CARGO_PKG_VERSION").as_bytes());
526 part(&mut material, request.request_id.as_str().as_bytes());
530 part(&mut material, request.request.source.as_str().as_bytes());
531 part(&mut material, principal);
532 RequestKey {
533 id: tagged_digest(KEY_TAG, &material),
534 material,
535 scope_id: scope_id(principal),
536 namespace: namespace(plan, policy),
537 }
538}
539
540#[cfg(test)]
541mod tests {
542 use std::collections::BTreeMap;
543
544 use rustev_contract::ids::{ArtifactId, DescriptorId, PlanId};
545 use rustev_contract::optimization::{EvictionOrder, ExpiryBasis, OptimizationAdmission};
546
547 use super::*;
548
549 fn digest(byte: char) -> String {
550 format!("sha256:{}", byte.to_string().repeat(64))
551 }
552
553 fn policy(plan: char) -> (PlanId, OptimizationPolicy) {
554 (
555 PlanId::parse(&digest(plan)).unwrap(),
556 OptimizationPolicy {
557 key_schema_version: 1,
558 cache_namespace_version: 1,
559 admission: OptimizationAdmission::Refuse,
560 expiry: ExpiryBasis::InjectedRuntimeTime,
561 batch: None,
562 memory_cache: Some(memory(4096, 2, 2048, 2048)),
563 },
564 )
565 }
566
567 fn memory(max_bytes: u64, max_entries: u64, entry: u64, scope: u64) -> MemoryCachePolicy {
568 MemoryCachePolicy {
569 acquisition_pages: false,
570 max_bytes,
571 max_entries,
572 max_entry_bytes: entry,
573 max_scope_bytes: scope,
574 ttl_ms: 10,
575 eviction: EvictionOrder::Fifo,
576 }
577 }
578
579 fn key_in(plan: char, principal: &[u8], step: &str) -> RequestKey {
580 let (plan_id, policy) = policy(plan);
581 request_key(KeyParts {
582 plan_id: &plan_id,
583 policy: &policy,
584 principal,
585 backend_artifact: &ArtifactId::parse(&digest('2')).unwrap(),
586 backend_descriptor: &DescriptorId::parse(&digest('3')).unwrap(),
587 step,
588 instance: &["one".into()],
589 projection: b"projection",
590 })
591 }
592
593 fn key(principal: &[u8]) -> RequestKey {
594 key_in('1', principal, "classify")
595 }
596
597 fn output(label: &str) -> RawOutput {
598 RawOutput::Scores(BTreeMap::from([(label.into(), 1.0)]))
599 }
600
601 fn write(attempt: &str, generation: u64) -> CacheWrite {
602 CacheWrite {
603 output: output(&"a".repeat(700)),
604 source_attempt_id: attempt.into(),
605 target: 0,
606 captured_generation: generation,
607 now: 0,
608 }
609 }
610
611 fn size(key: &RequestKey) -> u64 {
613 let optimizer = Optimizer::default();
614 let roomy = memory(1 << 20, 64, 1 << 20, 1 << 20);
615 optimizer
616 .insert(&roomy, key.clone(), write("x", 0))
617 .unwrap();
618 optimizer.total_bytes(&key.namespace)
619 }
620
621 #[test]
622 fn scope_and_instance_arity_are_part_of_the_complete_key() {
623 let a = key(b"tenant-a");
624 let b = key(b"tenant-b");
625 assert_ne!(a.id, b.id);
626 assert_ne!(a.scope_id, b.scope_id);
627 assert_ne!(a.material, b.material);
628 let (plan_id, policy) = policy('1');
630 let with = |instance: &[String]| {
631 request_key(KeyParts {
632 plan_id: &plan_id,
633 policy: &policy,
634 principal: b"t",
635 backend_artifact: &ArtifactId::parse(&digest('2')).unwrap(),
636 backend_descriptor: &DescriptorId::parse(&digest('3')).unwrap(),
637 step: "s",
638 instance,
639 projection: b"p",
640 })
641 .id
642 };
643 assert_ne!(with(&["ab".into()]), with(&["a".into(), "b".into()]));
644 }
645
646 #[test]
647 fn memory_cache_checks_expiry_collision_and_invalidation_generation() {
648 let optimizer = Optimizer::default();
649 let memory = policy('1').1.memory_cache.unwrap();
650 let key = key(b"tenant");
651 optimizer
652 .insert(
653 &memory,
654 key.clone(),
655 CacheWrite {
656 output: output("a"),
657 source_attempt_id: "attempt-1".into(),
658 target: 0,
659 captured_generation: 0,
660 now: 5,
661 },
662 )
663 .unwrap();
664 assert_eq!(optimizer.lookup(&key, 14).unwrap().age_ms, 9);
665 assert_eq!(optimizer.lookup(&key, 15), Err(CacheMiss::Expired));
666 assert_eq!(optimizer.lookup(&key, 15), Err(CacheMiss::Absent));
667
668 optimizer
669 .insert(&memory, key.clone(), write("a-2", 0))
670 .unwrap();
671 optimizer.force_collision(&key, b"other material".to_vec());
672 assert_eq!(optimizer.lookup(&key, 1), Err(CacheMiss::Collision));
673 assert_eq!(
674 optimizer.insert(&memory, key.clone(), write("a-3", 0)),
675 Err(CacheRefusal::Collision)
676 );
677
678 let result = optimizer.invalidate(&InvalidationSelector::Namespace {
679 namespace: key.namespace.clone(),
680 });
681 assert_eq!(result.generation, 1);
682 assert_eq!(result.removed_entries, 1);
683 assert_eq!(
686 optimizer.insert(&memory, key.clone(), write("late", 0)),
687 Err(CacheRefusal::StaleGeneration)
688 );
689 assert_eq!(optimizer.lookup(&key, 1), Err(CacheMiss::Absent));
690 optimizer
691 .insert(&memory, key.clone(), write("fresh", 1))
692 .unwrap();
693 assert_eq!(
694 optimizer.lookup(&key, 1).unwrap().source_attempt_id,
695 "fresh"
696 );
697 }
698
699 #[test]
700 fn an_oversized_entry_is_refused_before_anything_is_evicted() {
701 let optimizer = Optimizer::default();
702 let a = key_in('1', b"tenant", "a");
703 let n = size(&a);
704 let tight = memory(4 * n, 2, n, 2 * n);
705 optimizer.insert(&tight, a.clone(), write("a", 0)).unwrap();
706 let big = CacheWrite {
707 output: output(&"b".repeat(800)),
708 ..write("b", 0)
709 };
710 assert_eq!(
711 optimizer.insert(&tight, key_in('1', b"tenant", "b"), big),
712 Err(CacheRefusal::EntryTooLarge)
713 );
714 assert!(optimizer.lookup(&a, 0).is_ok(), "nothing was evicted");
715 }
716
717 #[test]
718 fn total_and_count_bounds_evict_the_namespace_fifo_head() {
719 let optimizer = Optimizer::default();
720 let a = key(b"tenant-a");
721 let n = size(&a);
722 let total = memory(2 * n + n / 2, 8, n, 2 * n + n / 2);
723 let b = key(b"tenant-b");
724 let c = key(b"tenant-c");
725 optimizer.insert(&total, a.clone(), write("a", 0)).unwrap();
726 optimizer.insert(&total, b.clone(), write("b", 0)).unwrap();
727 let admission = optimizer.insert(&total, c.clone(), write("c", 0)).unwrap();
728 assert_eq!(admission.evicted_entries, 1);
729 assert_eq!(optimizer.lookup(&a, 0), Err(CacheMiss::Absent));
730 assert!(optimizer.lookup(&b, 0).is_ok());
731 assert!(optimizer.lookup(&c, 0).is_ok());
732
733 let optimizer = Optimizer::default();
734 let count = memory(100 * n, 2, n, 100 * n);
735 optimizer.insert(&count, a.clone(), write("a", 0)).unwrap();
736 optimizer.insert(&count, b.clone(), write("b", 0)).unwrap();
737 let admission = optimizer.insert(&count, c.clone(), write("c", 0)).unwrap();
738 assert_eq!(admission.evicted_entries, 1);
739 assert_eq!(optimizer.lookup(&a, 0), Err(CacheMiss::Absent));
740 }
741
742 #[test]
743 fn a_scope_bound_evicts_only_that_scopes_entries() {
744 let optimizer = Optimizer::default();
745 let b = key_in('1', b"tenant-b", "b");
747 let n = size(&b);
748 let scoped = memory(100 * n, 64, n + 8, 2 * n + n / 2);
750 optimizer.insert(&scoped, b.clone(), write("b", 0)).unwrap();
751 let a1 = key_in('1', b"tenant-a", "a1");
752 let a2 = key_in('1', b"tenant-a", "a2");
753 let a3 = key_in('1', b"tenant-a", "a3");
754 optimizer
755 .insert(&scoped, a1.clone(), write("a1", 0))
756 .unwrap();
757 optimizer
758 .insert(&scoped, a2.clone(), write("a2", 0))
759 .unwrap();
760 let admission = optimizer
761 .insert(&scoped, a3.clone(), write("a3", 0))
762 .unwrap();
763 assert_eq!(admission.evicted_entries, 1);
764 assert_eq!(optimizer.lookup(&a1, 0), Err(CacheMiss::Absent));
765 assert!(optimizer.lookup(&a2, 0).is_ok());
766 assert!(optimizer.lookup(&a3, 0).is_ok());
767 assert!(
768 optimizer.lookup(&b, 0).is_ok(),
769 "tenant A's traffic never evicts tenant B"
770 );
771 }
772
773 #[test]
774 fn one_plans_bounds_never_evict_another_plans_entries() {
775 let optimizer = Optimizer::default();
776 let roomy = memory(100_000, 64, 2048, 100_000);
777 let tiny = memory(2048, 1, 2048, 2048);
778 let other = key_in('7', b"tenant", "x");
779 optimizer
780 .insert(&roomy, other.clone(), write("x", 0))
781 .unwrap();
782 for step in ["a", "b", "c"] {
783 optimizer
784 .insert(&tiny, key_in('1', b"tenant", step), write(step, 0))
785 .unwrap();
786 }
787 assert!(optimizer.lookup(&other, 0).is_ok());
788 optimizer.invalidate(&InvalidationSelector::Namespace {
790 namespace: key_in('1', b"tenant", "a").namespace,
791 });
792 assert_eq!(optimizer.generation(&other.namespace), 0);
793 assert!(optimizer.lookup(&other, 0).is_ok());
794 }
795
796 #[test]
797 fn scope_invalidation_is_exact() {
798 let optimizer = Optimizer::default();
799 let roomy = memory(100_000, 64, 2048, 100_000);
800 let a = key(b"tenant-a");
801 let b = key(b"tenant-b");
802 optimizer.insert(&roomy, a.clone(), write("a", 0)).unwrap();
803 optimizer.insert(&roomy, b.clone(), write("b", 0)).unwrap();
804 let result = optimizer.invalidate(&InvalidationSelector::Scope {
805 namespace: a.namespace.clone(),
806 scope_id: a.scope_id.clone(),
807 });
808 assert_eq!(result.removed_entries, 1);
809 assert_eq!(optimizer.lookup(&a, 0), Err(CacheMiss::Absent));
810 assert_eq!(optimizer.lookup(&b, 0), Err(CacheMiss::Expired));
813 }
814
815 #[test]
816 fn lazily_deleted_queue_slots_stay_bounded() {
817 let optimizer = Optimizer::default();
818 let roomy = memory(100_000, 64, 2048, 100_000);
819 let key = key(b"tenant");
820 for n in 0..500 {
821 optimizer
822 .insert(&roomy, key.clone(), write(&format!("a{n}"), 0))
823 .unwrap();
824 }
825 let (fifo, scoped) = optimizer.queue_lengths(&key.namespace);
826 assert!(fifo <= 20 && scoped <= 20, "{fifo} {scoped}");
827 }
828}