Skip to main content

rustev_runtime/
optimization.rs

1//! Bounded reusable-work state for spec 017.
2//!
3//! State is kept per cache namespace, which is per plan: one plan's bounds
4//! never evict another plan's entries, and a per-scope bound evicts only the
5//! inserting scope's own oldest entries (I-2).
6
7use 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
28/// Every output-affecting member available at the runtime seam.
29///
30/// The plan id transitively identifies operation, task, question, ordered
31/// options or levels, ordered candidates, preprocessing, tokenization,
32/// truncation, precision, backend configuration, requested privacy,
33/// served-identity requirements, step semantics, calibration, fallback,
34/// protocol, adapter, artifact binding and the optimization policy. The
35/// canonical projection identifies the concrete projected values, instance,
36/// canonical inputs and provenance-relevant revisions. Descriptor and
37/// artifact ids bind the selected installed target. `principal` is the
38/// host-supplied complete authorized-scope handle. Runtime and key-schema
39/// versions prevent reuse across implementation or key interpretation.
40pub(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/// Why a lookup did not hit.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub(crate) enum CacheMiss {
123    Absent,
124    Expired,
125    /// Equal digests over different key material: an integrity event.
126    Collision,
127}
128
129/// Why a validated output was not admitted. Nothing is touched before the
130/// admissibility preconditions hold.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub(crate) enum CacheRefusal {
133    /// The output has no canonical form.
134    Uncanonical,
135    /// The entry exceeds the per-entry bound.
136    EntryTooLarge,
137    /// The namespace generation advanced after the request captured it.
138    StaleGeneration,
139    /// A retained entry has the same digest and different key material.
140    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    /// Admission sequence, which makes lazily deleted queue slots
176    /// recognizable.
177    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        // Queue slots are deleted lazily; compact when stale slots dominate
208        // so the queues stay proportional to the live entries.
209        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    /// Evict the oldest live entry of `scope`, or of the namespace.
227    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/// Which retained entries an invalidation selects.
241#[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    /// The namespace generation after the invalidation. Results captured
250    /// under an earlier generation are never stored.
251    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        // Core validation guarantees max_entry_bytes <= max_scope_bytes <=
371        // max_bytes and max_entries >= 1, so an entry within its own bound
372        // is always admissible after eviction.
373        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        // Replacing an equivalent entry is not an eviction.
389        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    /// Advance the namespace generation, then remove the selected entries.
454    /// Any result captured under the old generation is refused at insert.
455    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
512/// Acquisition identity and complete authorized scope, in the versioned cache namespace.
513pub(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    // Like `request_key`, bind the runtime version to prevent reuse across
523    // implementations (spec 017, Implementation record: complete memory-cache
524    // key). Spec 036 3.13.7 uses that versioned key scheme.
525    part(&mut material, env!("CARGO_PKG_VERSION").as_bytes());
526    // `AcquisitionRequestId` is the identity of the whole
527    // `rustev.acquisition-request/1` document, which names its `operation`
528    // (spec 036 3.3.1), so two operations never share a key.
529    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    /// The accounted size of one `write("x", ..)` entry under `key`.
612    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        // ["ab"] and ["a", "b"] are different instances.
629        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        // A result captured before the invalidation is never stored and so
684        // never served to a later request (I-3).
685        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        // Tenant B's entry is the global FIFO head.
746        let b = key_in('1', b"tenant-b", "b");
747        let n = size(&b);
748        // Step names and attempt ids differ by a byte or two.
749        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        // Invalidating one namespace leaves the other's generation alone.
789        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        // The generation is namespace-wide, which is conservative: the other
811        // scope's entry is no longer served.
812        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}