1use std::collections::{BTreeMap, VecDeque};
8use std::sync::{Arc, 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
28impl RequestKey {
29 pub(crate) fn from_material(material: Vec<u8>, scope_id: String, namespace: String) -> Self {
32 RequestKey {
33 id: tagged_digest(KEY_TAG, &material),
34 material,
35 scope_id,
36 namespace,
37 }
38 }
39}
40
41pub(crate) struct KeyParts<'a> {
56 pub plan_id: &'a PlanId,
57 pub profile: &'a str,
58 pub policy: &'a OptimizationPolicy,
59 pub principal: &'a [u8],
60 pub backend_artifact: &'a ArtifactId,
61 pub backend_descriptor: &'a DescriptorId,
62 pub step: &'a str,
63 pub instance: &'a [String],
64 pub projection: &'a [u8],
65}
66
67fn part(out: &mut Vec<u8>, bytes: &[u8]) {
68 out.extend_from_slice(&(bytes.len() as u64).to_be_bytes());
69 out.extend_from_slice(bytes);
70}
71
72pub(crate) fn request_key(p: KeyParts<'_>) -> RequestKey {
73 let mut material = Vec::new();
74 part(&mut material, p.plan_id.as_str().as_bytes());
75 part(&mut material, p.profile.as_bytes());
76 part(&mut material, &p.policy.key_schema_version.to_be_bytes());
77 part(
78 &mut material,
79 &p.policy.cache_namespace_version.to_be_bytes(),
80 );
81 part(&mut material, env!("CARGO_PKG_VERSION").as_bytes());
82 part(&mut material, p.principal);
83 part(&mut material, p.backend_artifact.as_str().as_bytes());
84 part(&mut material, p.backend_descriptor.as_str().as_bytes());
85 part(&mut material, p.step.as_bytes());
86 part(&mut material, &(p.instance.len() as u64).to_be_bytes());
87 for member in p.instance {
88 part(&mut material, member.as_bytes());
89 }
90 part(&mut material, p.projection);
91 let scope_id = scope_id(p.principal);
92 let namespace = namespace(p.plan_id, p.policy);
93 RequestKey {
94 id: tagged_digest(KEY_TAG, &material),
95 material,
96 scope_id,
97 namespace,
98 }
99}
100
101pub(crate) fn profile_of(plan: &rustev_contract::plan::Plan) -> String {
107 use rustev_contract::plan::ProfileChain;
108 match plan.instantiation.as_ref().map(|i| &i.profile) {
109 None | Some(ProfileChain::None) => "none".into(),
110 Some(ProfileChain::Chain(entries)) => {
112 profile_ids(&entries.iter().map(|e| e.id.as_str()).collect::<Vec<_>>())
113 }
114 }
115}
116
117fn profile_ids(ids: &[&str]) -> String {
118 serde_json::to_string(ids).expect("a list of strings serializes")
119}
120
121pub(crate) fn scope_id(principal: &[u8]) -> String {
122 tagged_digest("rustev.optimization-scope/1", principal)
123}
124
125pub(crate) fn namespace(plan_id: &PlanId, policy: &OptimizationPolicy) -> String {
126 format!(
127 "{}/{}/{}",
128 plan_id, policy.key_schema_version, policy.cache_namespace_version
129 )
130}
131
132#[derive(Debug, Clone, PartialEq)]
133pub(crate) struct CachedOutput {
134 pub output: RawOutput,
135 pub source_attempt_id: String,
136 pub target: u32,
137 pub entry_id: String,
138 pub age_ms: u64,
139 pub generation: u64,
140}
141
142pub(crate) struct CacheWrite {
143 pub output: RawOutput,
144 pub source_attempt_id: String,
145 pub target: u32,
146 pub captured_generation: u64,
147 pub now: Instant,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub(crate) struct CacheAdmission {
152 pub entry_id: String,
153 pub evicted_entries: u64,
154 pub evicted_bytes: u64,
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub(crate) enum CacheMiss {
160 Absent,
161 Expired,
162 Collision,
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub(crate) enum CacheRefusal {
170 Uncanonical,
172 EntryTooLarge,
174 StaleGeneration,
176 Collision,
178 NoCapacity,
182}
183
184#[derive(Debug, Clone)]
185enum CachedValue {
186 Semantic(RawOutput),
187 Page(Vec<u8>),
188}
189
190struct StoredWrite {
191 output: CachedValue,
192 source_attempt_id: String,
193 target: u32,
194 captured_generation: u64,
195 now: Instant,
196}
197
198pub(crate) struct PageWrite {
199 pub bytes: Vec<u8>,
200 pub attempt: String,
201 pub generation: u64,
202 pub now: Instant,
203}
204
205#[derive(Debug, Clone)]
206struct Entry {
207 key: RequestKey,
208 output: CachedValue,
209 source_attempt_id: String,
210 target: u32,
211 entry_id: String,
212 created: Instant,
213 expires: Instant,
214 generation: u64,
215 bytes: u64,
216 seq: u64,
219}
220
221type Slot = (String, u64);
222
223#[derive(Debug, Default)]
224struct NamespaceState {
225 generation: u64,
226 entries: BTreeMap<String, Entry>,
227 fifo: VecDeque<Slot>,
228 scope_fifo: BTreeMap<String, VecDeque<Slot>>,
229 scope_bytes: BTreeMap<String, u64>,
230 total_bytes: u64,
231 next_seq: u64,
232 decisions: BTreeMap<String, decision::DecisionSlot>,
235 decision_fifo: VecDeque<Slot>,
236}
237
238impl NamespaceState {
239 fn live(&self, slot: &Slot) -> bool {
240 self.entries.get(&slot.0).is_some_and(|e| e.seq == slot.1)
241 }
242
243 fn remove(&mut self, id: &str) -> Option<Entry> {
244 let entry = self.entries.remove(id)?;
245 self.total_bytes = self.total_bytes.saturating_sub(entry.bytes);
246 if let Some(bytes) = self.scope_bytes.get_mut(&entry.key.scope_id) {
247 *bytes = bytes.saturating_sub(entry.bytes);
248 if *bytes == 0 {
249 self.scope_bytes.remove(&entry.key.scope_id);
250 }
251 }
252 if self.fifo.len() > 2 * self.entries.len() + 16 {
255 let entries = &self.entries;
256 self.fifo
257 .retain(|s| entries.get(&s.0).is_some_and(|e| e.seq == s.1));
258 }
259 if let Some(queue) = self.scope_fifo.get_mut(&entry.key.scope_id) {
260 let live = self.scope_bytes.contains_key(&entry.key.scope_id);
261 if !live {
262 self.scope_fifo.remove(&entry.key.scope_id);
263 } else if queue.len() > 16 {
264 let entries = &self.entries;
265 queue.retain(|s| entries.get(&s.0).is_some_and(|e| e.seq == s.1));
266 }
267 }
268 Some(entry)
269 }
270
271 fn evict_oldest(&mut self, scope: Option<&str>) -> Option<Entry> {
273 loop {
274 let slot = match scope {
275 Some(scope) => self.scope_fifo.get_mut(scope)?.pop_front()?,
276 None => self.fifo.pop_front()?,
277 };
278 if self.live(&slot) {
279 return self.remove(&slot.0);
280 }
281 }
282 }
283}
284
285#[derive(Debug, Clone, PartialEq, Eq)]
287pub(crate) enum InvalidationSelector {
288 Namespace { namespace: String },
289 Scope { namespace: String, scope_id: String },
290}
291
292#[derive(Debug, Clone, PartialEq, Eq)]
293pub struct InvalidationResult {
294 pub generation: u64,
297 pub removed_entries: u64,
298 pub removed_bytes: u64,
299 pub persistent: Option<PersistentInvalidation>,
302 pub complete: bool,
305}
306
307#[derive(Debug, Clone, PartialEq, Eq)]
309pub enum PersistentInvalidation {
310 Pending,
312 Done { generation: u64, removed: u64 },
314 Failed { detail: String },
317}
318
319#[derive(Debug, Default)]
320pub(crate) struct Optimizer {
321 namespaces: Mutex<BTreeMap<String, NamespaceState>>,
322 shared: Mutex<BTreeMap<String, crate::driver::SharedCalls>>,
326}
327
328impl Optimizer {
329 pub(crate) fn shared_calls(
330 &self,
331 namespace: &str,
332 max_slots: usize,
333 ) -> crate::driver::SharedCalls {
334 self.shared
335 .lock()
336 .unwrap_or_else(|e| e.into_inner())
337 .entry(namespace.to_string())
338 .or_insert_with(|| Arc::new(crate::shared::Registry::new(max_slots)))
339 .clone()
340 }
341
342 fn lock(&self) -> std::sync::MutexGuard<'_, BTreeMap<String, NamespaceState>> {
343 self.namespaces.lock().unwrap_or_else(|e| e.into_inner())
344 }
345
346 pub(crate) fn generation(&self, namespace: &str) -> u64 {
347 self.lock().get(namespace).map_or(0, |ns| ns.generation)
348 }
349
350 fn lookup_entry(&self, key: &RequestKey, now: Instant) -> Result<Entry, CacheMiss> {
351 let mut namespaces = self.lock();
352 let Some(ns) = namespaces.get_mut(&key.namespace) else {
353 return Err(CacheMiss::Absent);
354 };
355 let Some(entry) = ns.entries.get(&key.id) else {
356 return Err(CacheMiss::Absent);
357 };
358 if entry.key.material != key.material {
359 return Err(CacheMiss::Collision);
360 }
361 if entry.expires <= now || entry.generation != ns.generation {
362 ns.remove(&key.id);
363 return Err(CacheMiss::Expired);
364 }
365 Ok(entry.clone())
366 }
367
368 pub(crate) fn lookup(&self, key: &RequestKey, now: Instant) -> Result<CachedOutput, CacheMiss> {
369 let e = self.lookup_entry(key, now)?;
370 let CachedValue::Semantic(output) = e.output else {
371 return Err(CacheMiss::Collision);
372 };
373 Ok(CachedOutput {
374 output,
375 source_attempt_id: e.source_attempt_id,
376 target: e.target,
377 entry_id: e.entry_id,
378 age_ms: now.saturating_sub(e.created),
379 generation: e.generation,
380 })
381 }
382
383 pub(crate) fn lookup_page(
384 &self,
385 key: &RequestKey,
386 now: Instant,
387 ) -> Result<(Vec<u8>, u64), CacheMiss> {
388 let e = self.lookup_entry(key, now)?;
389 let CachedValue::Page(bytes) = e.output else {
390 return Err(CacheMiss::Collision);
391 };
392 Ok((bytes, now.saturating_sub(e.created)))
393 }
394
395 pub(crate) fn insert_page(
396 &self,
397 policy: &MemoryCachePolicy,
398 key: RequestKey,
399 page: PageWrite,
400 ) -> Result<CacheAdmission, CacheRefusal> {
401 self.insert_value(
402 policy,
403 key,
404 StoredWrite {
405 source_attempt_id: page.attempt,
406 target: 0,
407 captured_generation: page.generation,
408 now: page.now,
409 output: CachedValue::Page(page.bytes),
410 },
411 )
412 }
413
414 pub(crate) fn insert(
415 &self,
416 policy: &MemoryCachePolicy,
417 key: RequestKey,
418 write: CacheWrite,
419 ) -> Result<CacheAdmission, CacheRefusal> {
420 self.insert_value(
421 policy,
422 key,
423 StoredWrite {
424 output: CachedValue::Semantic(write.output),
425 source_attempt_id: write.source_attempt_id,
426 target: write.target,
427 captured_generation: write.captured_generation,
428 now: write.now,
429 },
430 )
431 }
432
433 fn insert_value(
434 &self,
435 policy: &MemoryCachePolicy,
436 key: RequestKey,
437 write: StoredWrite,
438 ) -> Result<CacheAdmission, CacheRefusal> {
439 let output_bytes = match &write.output {
440 CachedValue::Semantic(output) => {
441 record_canonical_bytes(output).map_err(|_| CacheRefusal::Uncanonical)?
442 }
443 CachedValue::Page(bytes) => bytes.clone(),
444 };
445 let bytes = key
446 .material
447 .len()
448 .saturating_add(output_bytes.len())
449 .saturating_add(write.source_attempt_id.len()) as u64;
450 if bytes > policy.max_entry_bytes {
454 return Err(CacheRefusal::EntryTooLarge);
455 }
456 let mut namespaces = self.lock();
457 let ns = namespaces.entry(key.namespace.clone()).or_default();
458 if ns.generation != write.captured_generation {
459 return Err(CacheRefusal::StaleGeneration);
460 }
461 if ns
462 .entries
463 .get(&key.id)
464 .is_some_and(|entry| entry.key.material != key.material)
465 {
466 return Err(CacheRefusal::Collision);
467 }
468 ns.remove(&key.id);
470 let mut evicted_entries = 0_u64;
471 let mut evicted_bytes = 0_u64;
472 let mut evicted = |entry: Entry| {
473 evicted_entries += 1;
474 evicted_bytes = evicted_bytes.saturating_add(entry.bytes);
475 };
476 while ns
477 .scope_bytes
478 .get(&key.scope_id)
479 .copied()
480 .unwrap_or(0)
481 .saturating_add(bytes)
482 > policy.max_scope_bytes
483 {
484 let Some(entry) = ns.evict_oldest(Some(&key.scope_id)) else {
485 break;
486 };
487 evicted(entry);
488 }
489 while ns.entries.len() as u64 >= policy.max_entries
490 || ns.total_bytes.saturating_add(bytes) > policy.max_bytes
491 {
492 let Some(entry) = ns.evict_oldest(None) else {
493 break;
494 };
495 evicted(entry);
496 }
497 let mut entry_material = key.material.clone();
498 part(&mut entry_material, &output_bytes);
499 part(&mut entry_material, write.source_attempt_id.as_bytes());
500 let entry_id = tagged_digest(ENTRY_TAG, &entry_material);
501 let seq = ns.next_seq;
502 ns.next_seq += 1;
503 *ns.scope_bytes.entry(key.scope_id.clone()).or_default() += bytes;
504 ns.total_bytes += bytes;
505 ns.fifo.push_back((key.id.clone(), seq));
506 ns.scope_fifo
507 .entry(key.scope_id.clone())
508 .or_default()
509 .push_back((key.id.clone(), seq));
510 let generation = ns.generation;
511 ns.entries.insert(
512 key.id.clone(),
513 Entry {
514 key,
515 output: write.output,
516 source_attempt_id: write.source_attempt_id,
517 target: write.target,
518 entry_id: entry_id.clone(),
519 created: write.now,
520 expires: write.now.saturating_add(policy.ttl_ms),
521 generation,
522 bytes,
523 seq,
524 },
525 );
526 Ok(CacheAdmission {
527 entry_id,
528 evicted_entries,
529 evicted_bytes,
530 })
531 }
532
533 pub(crate) fn invalidate(&self, selector: &InvalidationSelector) -> InvalidationResult {
536 let namespace = match selector {
537 InvalidationSelector::Namespace { namespace }
538 | InvalidationSelector::Scope { namespace, .. } => namespace,
539 };
540 let mut namespaces = self.lock();
541 let ns = namespaces.entry(namespace.clone()).or_default();
542 ns.generation = ns.generation.saturating_add(1);
543 let ids: Vec<String> = ns
544 .entries
545 .iter()
546 .filter(|(_, entry)| match selector {
547 InvalidationSelector::Namespace { .. } => true,
548 InvalidationSelector::Scope { scope_id, .. } => &entry.key.scope_id == scope_id,
549 })
550 .map(|(id, _)| id.clone())
551 .collect();
552 let mut removed_bytes = 0;
553 for id in &ids {
554 if let Some(entry) = ns.remove(id) {
555 removed_bytes += entry.bytes;
556 }
557 }
558 let decisions: Vec<String> = ns
559 .decisions
560 .iter()
561 .filter(|(_, slot)| match selector {
562 InvalidationSelector::Namespace { .. } => true,
563 InvalidationSelector::Scope { scope_id, .. } => &slot.key.scope_id == scope_id,
564 })
565 .map(|(id, _)| id.clone())
566 .collect();
567 for id in &decisions {
568 if let Some(slot) = ns.decisions.remove(id) {
569 removed_bytes += slot.bytes;
570 }
571 }
572 InvalidationResult {
573 generation: ns.generation,
574 removed_entries: (ids.len() + decisions.len()) as u64,
575 removed_bytes,
576 persistent: None,
577 complete: true,
578 }
579 }
580
581 #[cfg(test)]
582 fn force_collision(&self, key: &RequestKey, other_material: Vec<u8>) {
583 let mut namespaces = self.lock();
584 if let Some(entry) = namespaces
585 .get_mut(&key.namespace)
586 .and_then(|ns| ns.entries.get_mut(&key.id))
587 {
588 entry.key.material = other_material;
589 }
590 }
591
592 #[cfg(test)]
593 fn total_bytes(&self, namespace: &str) -> u64 {
594 self.lock().get(namespace).map_or(0, |ns| ns.total_bytes)
595 }
596
597 #[cfg(test)]
598 fn queue_lengths(&self, namespace: &str) -> (usize, usize) {
599 let namespaces = self.lock();
600 let ns = &namespaces[namespace];
601 (
602 ns.fifo.len(),
603 ns.scope_fifo.values().map(VecDeque::len).sum(),
604 )
605 }
606}
607
608pub(crate) fn acquisition_key(
610 plan: &PlanId,
611 policy: &OptimizationPolicy,
612 principal: &[u8],
613 request: &rustev_core::evaluate::PendingAcquisition,
614) -> RequestKey {
615 let mut material = Vec::new();
616 part(&mut material, b"acquisition-page");
617 part(&mut material, &policy.key_schema_version.to_be_bytes());
618 part(&mut material, env!("CARGO_PKG_VERSION").as_bytes());
622 part(&mut material, request.request_id.as_str().as_bytes());
626 part(&mut material, request.request.source.as_str().as_bytes());
627 part(&mut material, principal);
628 RequestKey {
629 id: tagged_digest(KEY_TAG, &material),
630 material,
631 scope_id: scope_id(principal),
632 namespace: namespace(plan, policy),
633 }
634}
635
636pub(crate) mod decision {
641 use std::collections::{BTreeMap, BTreeSet};
645 use std::sync::{Arc, Mutex, MutexGuard};
646
647 use rustev_contract::cache_entry::EntryGenerations;
648 use rustev_contract::canonical::{record_canonical_bytes, tagged_digest};
649 use rustev_contract::ids::{ContentDigest, PlanId};
650 use rustev_contract::optimization::{
651 DecisionCachePolicy, DecisionCacheTier, OptimizationPolicy,
652 };
653 use rustev_contract::output::RawOutput;
654 use rustev_contract::scope::Handle;
655 use rustev_contract::snapshot::Snapshot;
656 use rustev_contract::time::Timestamp;
657
658 use super::{CacheRefusal, Optimizer, RequestKey, namespace, part, profile_of, scope_id};
659
660 const DECISION_KEY_TAG: &str = "rustev.decision-key/1";
661 const DECISION_ENTRY_TAG: &str = "rustev.decision-entry/1";
662 const DECISION_INPUT_TAG: &str = "rustev.decision-input/1";
663
664 pub(crate) struct DecisionKeyParts<'a> {
668 pub schema: &'a [String],
669 pub plan_id: &'a PlanId,
670 pub profile: &'a str,
671 pub policy: &'a OptimizationPolicy,
672 pub principal: &'a [u8],
673 pub dimensions: &'a BTreeMap<String, Handle>,
674 pub snapshot: &'a Snapshot,
675 }
676
677 #[derive(Debug, Clone, PartialEq, Eq)]
680 pub(crate) enum Unkeyed {
681 DimensionMissing(String),
683 Ineligible(String),
686 }
687
688 impl Unkeyed {
689 pub(crate) fn diagnostic(&self) -> String {
690 match self {
691 Unkeyed::DimensionMissing(d) => format!("key_dimension_missing: {d:?}"),
692 Unkeyed::Ineligible(why) => format!("key_ineligible: {why}"),
693 }
694 }
695 }
696
697 pub(crate) fn decision_key(p: DecisionKeyParts<'_>) -> Result<RequestKey, Unkeyed> {
702 if let Some(extra) = p
703 .dimensions
704 .keys()
705 .find(|d| !p.schema.contains(&format!("dimension:{d}")))
706 {
707 return Err(Unkeyed::Ineligible(format!(
708 "dimension {extra:?} is not declared by the plan"
709 )));
710 }
711 let mut material = Vec::new();
712 for member in p.schema {
713 part(&mut material, member.as_bytes());
714 match member.as_str() {
715 "plan_id" => part(&mut material, p.plan_id.as_str().as_bytes()),
716 "key_schema_version" => {
717 part(&mut material, &p.policy.key_schema_version.to_be_bytes())
718 }
719 "cache_namespace_version" => part(
720 &mut material,
721 &p.policy.cache_namespace_version.to_be_bytes(),
722 ),
723 "runtime_version" => part(&mut material, env!("CARGO_PKG_VERSION").as_bytes()),
724 "scope" => part(&mut material, p.principal),
725 "profile" => part(&mut material, p.profile.as_bytes()),
726 m => {
727 if let Some(name) = m.strip_prefix("dimension:") {
728 let handle = p
729 .dimensions
730 .get(name)
731 .ok_or_else(|| Unkeyed::DimensionMissing(name.into()))?;
732 part(&mut material, handle.as_str().as_bytes());
733 } else if let Some(name) = m.strip_prefix("input:") {
734 let entries: Vec<_> = p
735 .snapshot
736 .entries
737 .iter()
738 .filter(|e| e.field == name)
739 .collect();
740 part(&mut material, &(entries.len() as u64).to_be_bytes());
741 for e in entries {
742 let bytes = record_canonical_bytes(e).map_err(|_| {
743 Unkeyed::Ineligible(format!("input {name:?} has no canonical form"))
744 })?;
745 part(
746 &mut material,
747 tagged_digest(DECISION_INPUT_TAG, &bytes).as_bytes(),
748 );
749 }
750 } else {
751 return Err(Unkeyed::Ineligible(format!("unknown key member {m:?}")));
752 }
753 }
754 }
755 }
756 Ok(RequestKey {
757 id: tagged_digest(DECISION_KEY_TAG, &material),
758 material,
759 scope_id: scope_id(p.principal),
760 namespace: namespace(p.plan_id, p.policy),
761 })
762 }
763
764 #[derive(Debug, Clone)]
769 pub(crate) struct ReusedRequest {
770 pub key: RequestKey,
771 pub output: RawOutput,
772 pub source_attempt_id: String,
773 }
774
775 pub(crate) type ReusedRequests = BTreeMap<(String, Vec<String>), ReusedRequest>;
777
778 #[derive(Debug, Clone)]
781 pub(crate) struct DecisionEntry {
782 pub requests: ReusedRequests,
783 pub source_run_record: ContentDigest,
784 pub created: Timestamp,
787 }
788
789 #[derive(Debug, Clone)]
791 pub(super) struct DecisionSlot {
792 pub(super) key: RequestKey,
793 entry: Arc<DecisionEntry>,
794 entry_id: String,
795 generation: u64,
796 pub(super) bytes: u64,
797 seq: u64,
798 }
799
800 #[derive(Debug, Clone)]
802 pub(crate) struct DecisionHit {
803 pub entry: Arc<DecisionEntry>,
804 pub entry_id: String,
805 pub age_ms: u64,
806 pub generations: Option<EntryGenerations>,
808 }
809
810 pub(crate) fn decision_entry_id(key: &RequestKey, source: &ContentDigest) -> String {
813 let mut material = key.material.clone();
814 part(&mut material, source.as_str().as_bytes());
815 tagged_digest(DECISION_ENTRY_TAG, &material)
816 }
817
818 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
820 pub(crate) enum DecisionMiss {
821 Absent,
822 Collision,
823 GenerationAdvanced,
824 FromFuture,
825 Expired,
826 }
827
828 impl DecisionMiss {
829 pub(crate) fn diagnostic(self) -> &'static str {
830 match self {
831 DecisionMiss::Absent => "decision cache miss",
832 DecisionMiss::Collision => "entry_collision",
833 DecisionMiss::GenerationAdvanced => "generation_advanced",
834 DecisionMiss::FromFuture => "entry_from_future",
835 DecisionMiss::Expired => "entry_expired",
836 }
837 }
838 }
839
840 pub(crate) fn decision_entry_bytes(
844 key: &RequestKey,
845 requests: &ReusedRequests,
846 ) -> Result<u64, CacheRefusal> {
847 let mut bytes = key.material.len() as u64;
848 for r in requests.values() {
849 let output =
850 record_canonical_bytes(&r.output).map_err(|_| CacheRefusal::Uncanonical)?;
851 bytes = bytes
852 .saturating_add(r.key.material.len() as u64)
853 .saturating_add(output.len() as u64)
854 .saturating_add(r.source_attempt_id.len() as u64);
855 }
856 Ok(bytes)
857 }
858
859 impl Optimizer {
860 pub(crate) fn lookup_decision(
864 &self,
865 key: &RequestKey,
866 now: Timestamp,
867 ttl_ms: u64,
868 ) -> Result<DecisionHit, DecisionMiss> {
869 let mut namespaces = self.lock();
870 let Some(ns) = namespaces.get_mut(&key.namespace) else {
871 return Err(DecisionMiss::Absent);
872 };
873 let Some(slot) = ns.decisions.get(&key.id) else {
874 return Err(DecisionMiss::Absent);
875 };
876 if slot.key.material != key.material {
877 return Err(DecisionMiss::Collision);
878 }
879 if slot.generation != ns.generation {
880 ns.decisions.remove(&key.id);
881 return Err(DecisionMiss::GenerationAdvanced);
882 }
883 let (now, created) = (now.as_ms(), slot.entry.created.as_ms());
884 if now < created {
885 return Err(DecisionMiss::FromFuture);
886 }
887 let age_ms = u64::try_from(now - created).unwrap_or(u64::MAX);
888 if age_ms > ttl_ms {
889 ns.decisions.remove(&key.id);
890 return Err(DecisionMiss::Expired);
891 }
892 Ok(DecisionHit {
893 entry: slot.entry.clone(),
894 entry_id: slot.entry_id.clone(),
895 age_ms,
896 generations: None,
897 })
898 }
899
900 pub(crate) fn insert_decision(
904 &self,
905 policy: &DecisionCachePolicy,
906 key: RequestKey,
907 entry: DecisionEntry,
908 captured_generation: u64,
909 ) -> Result<String, CacheRefusal> {
910 let bytes = decision_entry_bytes(&key, &entry.requests)?;
911 if bytes > policy.max_entry_bytes {
912 return Err(CacheRefusal::EntryTooLarge);
913 }
914 let Some(max_entries) = policy.max_entries.filter(|&n| n > 0) else {
917 return Err(CacheRefusal::NoCapacity);
918 };
919 let mut namespaces = self.lock();
920 let ns = namespaces.entry(key.namespace.clone()).or_default();
921 if ns.generation != captured_generation {
922 return Err(CacheRefusal::StaleGeneration);
923 }
924 if ns
925 .decisions
926 .get(&key.id)
927 .is_some_and(|slot| slot.key.material != key.material)
928 {
929 return Err(CacheRefusal::Collision);
930 }
931 ns.decisions.remove(&key.id);
932 while ns.decisions.len() as u64 >= max_entries {
933 let Some((id, seq)) = ns.decision_fifo.pop_front() else {
934 break;
935 };
936 if ns.decisions.get(&id).is_some_and(|slot| slot.seq == seq) {
937 ns.decisions.remove(&id);
938 }
939 }
940 if ns.decision_fifo.len() > 2 * ns.decisions.len() + 16 {
941 let decisions = &ns.decisions;
942 ns.decision_fifo
943 .retain(|(id, seq)| decisions.get(id).is_some_and(|slot| slot.seq == *seq));
944 }
945 let entry_id = decision_entry_id(&key, &entry.source_run_record);
946 let seq = ns.next_seq;
947 ns.next_seq += 1;
948 ns.decision_fifo.push_back((key.id.clone(), seq));
949 let generation = ns.generation;
950 ns.decisions.insert(
951 key.id.clone(),
952 DecisionSlot {
953 key,
954 entry: Arc::new(entry),
955 entry_id: entry_id.clone(),
956 generation,
957 bytes,
958 seq,
959 },
960 );
961 Ok(entry_id)
962 }
963 }
964
965 type At = (String, Vec<String>);
966
967 #[derive(Debug, Default)]
969 pub(crate) struct DecisionLog {
970 pub keys: BTreeMap<At, RequestKey>,
972 pub outputs: BTreeMap<At, RawOutput>,
974 pub reused: Vec<At>,
975 pub refreshed: Vec<At>,
976 pub mismatched: Vec<At>,
977 pub diagnostics: Vec<String>,
978 }
979
980 #[derive(Debug)]
983 pub(crate) struct DecisionRun {
984 pub closure: BTreeSet<String>,
986 pub key: Option<RequestKey>,
988 pub generation: u64,
991 pub captured: Option<EntryGenerations>,
994 pub hit: Option<DecisionHit>,
995 pub refused: Option<String>,
998 outcome: rustev_contract::optimization::DecisionCacheOutcome,
999 log: Mutex<DecisionLog>,
1000 }
1001
1002 impl DecisionRun {
1003 pub(crate) fn start(
1008 optimizer: &Optimizer,
1009 plan: &crate::PreparedPlan,
1010 req: &crate::DecisionRequest,
1011 dimensions: &BTreeMap<String, rustev_contract::scope::Handle>,
1012 live: bool,
1013 ) -> Option<DecisionRun> {
1014 use rustev_contract::optimization::{DecisionCacheOutcome, OptimizationAdmission};
1015 use rustev_contract::plan::PlanExecution;
1016 let policy = plan.optimization.as_ref()?;
1017 let cache = policy.decision_cache.as_ref()?;
1018 let PlanExecution::Declared(d) = &plan.compiled.plan.execution else {
1019 return None;
1020 };
1021 let compiled = d.decision_cache.as_ref()?;
1022 let mut run = DecisionRun {
1023 closure: compiled.refresh_closure.iter().cloned().collect(),
1024 key: None,
1025 generation: 0,
1026 captured: None,
1027 hit: None,
1028 refused: None,
1029 outcome: DecisionCacheOutcome::Miss,
1030 log: Mutex::default(),
1031 };
1032 let log = run.log.get_mut().unwrap_or_else(|e| e.into_inner());
1033 if live {
1035 run.outcome = DecisionCacheOutcome::Bypassed;
1036 log.diagnostics
1037 .push("live re-execution observes fresh backend work".into());
1038 return Some(run);
1039 }
1040 let key = decision_key(DecisionKeyParts {
1041 schema: &compiled.decision_key_schema,
1042 plan_id: &plan.compiled.id,
1043 profile: &profile_of(&plan.compiled.plan),
1044 policy,
1045 principal: &req.principal_handle,
1046 dimensions,
1047 snapshot: &req.snapshot,
1048 });
1049 match key {
1050 Err(unkeyed) => {
1051 let diagnostic = unkeyed.diagnostic();
1052 log.diagnostics.push(diagnostic.clone());
1053 if policy.admission == OptimizationAdmission::Refuse {
1054 run.outcome = DecisionCacheOutcome::Refused;
1055 run.refused = Some(diagnostic);
1056 } else {
1057 run.outcome = DecisionCacheOutcome::Bypassed;
1058 }
1059 }
1060 Ok(key) => {
1061 run.generation = optimizer.generation(&key.namespace);
1062 if cache.tiers.contains(&DecisionCacheTier::Memory) {
1063 match optimizer.lookup_decision(&key, req.evaluation_time, cache.ttl_ms) {
1064 Ok(hit) => run.hit = Some(hit),
1065 Err(miss) => log.diagnostics.push(miss.diagnostic().into()),
1066 }
1067 }
1068 run.key = Some(key);
1069 }
1070 }
1071 Some(run)
1072 }
1073
1074 pub(crate) async fn read_store(
1080 &mut self,
1081 optimizer: &Optimizer,
1082 policy: &OptimizationPolicy,
1083 cache: &DecisionCachePolicy,
1084 calls: &crate::store::StoreCtx<'_>,
1085 now: Timestamp,
1086 ) {
1087 use crate::store::{DecisionRead, read_decision};
1088 use rustev_contract::optimization::{
1089 DecisionCacheOutcome, OptimizationAdmission, StoreFailurePolicy,
1090 };
1091 if !cache.tiers.contains(&DecisionCacheTier::Persistent)
1092 || self.hit.is_some()
1093 || self.refused.is_some()
1094 {
1095 return;
1096 }
1097 let Some(key) = self.key.clone() else {
1098 return;
1099 };
1100 match read_decision(calls, &key, now).await {
1101 DecisionRead::Hit {
1102 entry,
1103 age_ms,
1104 generations,
1105 } => {
1106 let entry_id = decision_entry_id(&key, &entry.source_run_record);
1107 let entry = Arc::new(entry);
1108 if cache.tiers.contains(&DecisionCacheTier::Memory)
1109 && let Err(refusal) =
1110 optimizer.insert_decision(cache, key, (*entry).clone(), self.generation)
1111 {
1112 self.diagnose(crate::driver::cache_refusal_detail(refusal).into());
1113 }
1114 self.hit = Some(DecisionHit {
1115 entry,
1116 entry_id,
1117 age_ms,
1118 generations: Some(generations),
1119 });
1120 }
1121 DecisionRead::Miss {
1122 captured,
1123 diagnostic,
1124 } => {
1125 self.captured = captured;
1126 self.diagnose(diagnostic);
1127 }
1128 DecisionRead::Failed(detail) => {
1129 self.diagnose(detail.clone());
1130 if calls.persistent.on_store_failure == StoreFailurePolicy::FailClosed {
1131 self.key = None;
1132 if policy.admission == OptimizationAdmission::Refuse {
1133 self.outcome = DecisionCacheOutcome::Refused;
1134 self.refused = Some(detail);
1135 } else {
1136 self.outcome = DecisionCacheOutcome::Bypassed;
1137 }
1138 }
1139 }
1140 }
1141 }
1142
1143 pub(crate) fn log(&self) -> MutexGuard<'_, DecisionLog> {
1144 self.log.lock().unwrap_or_else(|e| e.into_inner())
1145 }
1146
1147 pub(crate) fn record(
1152 &self,
1153 requests: &[rustev_contract::run::RequestRecord],
1154 unresolved_volatile: &[String],
1155 ) -> rustev_contract::optimization::DecisionCacheRecord {
1156 use rustev_contract::optimization::{
1157 DecisionCacheOutcome, DecisionCacheRecord, MAX_OPTIMIZATION_DIAGNOSTICS,
1158 RefreshFailurePolicy,
1159 };
1160 use rustev_contract::run::RequestResult;
1161 let log = self.log();
1162 let index = |list: &[At]| {
1163 let mut found: Vec<u32> = list
1164 .iter()
1165 .filter_map(|(step, instance)| {
1166 requests
1167 .iter()
1168 .position(|r| &r.step == step && &r.instance == instance)
1169 })
1170 .map(|n| n as u32)
1171 .collect();
1172 found.sort_unstable();
1173 found
1174 };
1175 let mut record = DecisionCacheRecord {
1176 outcome: self.outcome,
1177 entry_id: None,
1178 source_run_record: None,
1179 age_ms: None,
1180 generations: None,
1181 reused: index(&log.reused),
1182 refreshed: index(&log.refreshed),
1183 mismatched: index(&log.mismatched),
1184 substituted: vec![],
1185 on_refresh_failure: None,
1186 diagnostics: vec![],
1187 };
1188 let mut diagnostics = log.diagnostics.clone();
1189 if let Some(hit) = &self.hit {
1190 record.outcome = if record.refreshed.is_empty() && record.mismatched.is_empty() {
1191 DecisionCacheOutcome::Hit
1192 } else {
1193 DecisionCacheOutcome::Refreshed
1194 };
1195 record.entry_id = Some(hit.entry_id.clone());
1196 record.source_run_record = Some(hit.entry.source_run_record.clone());
1197 record.age_ms = Some(hit.age_ms);
1198 record.generations = hit.generations;
1199 let mut failed: Vec<String> = unresolved_volatile
1200 .iter()
1201 .map(|v| format!("input {v:?}"))
1202 .collect();
1203 failed.extend(record.refreshed.iter().filter_map(|&n| {
1204 let r = &requests[n as usize];
1205 matches!(
1206 r.result,
1207 RequestResult::Failed(_) | RequestResult::NotSupplied
1208 )
1209 .then(|| format!("step {:?}", r.step))
1210 }));
1211 if !failed.is_empty() {
1212 record.on_refresh_failure = Some(RefreshFailurePolicy::Unresolved);
1213 diagnostics.push(format!("refresh_failed: {}", failed.join(", ")));
1214 }
1215 }
1216 for d in diagnostics {
1217 let d = crate::wait::cut(&d, crate::driver::DETAIL_BYTES);
1218 match record.diagnostics.len() {
1219 n if n + 1 < MAX_OPTIMIZATION_DIAGNOSTICS => record.diagnostics.push(d),
1220 n if n + 1 == MAX_OPTIMIZATION_DIAGNOSTICS => {
1221 record
1222 .diagnostics
1223 .push("further diagnostics dropped".into());
1224 }
1225 _ => {}
1226 }
1227 }
1228 record
1229 }
1230
1231 pub(crate) fn admissible(
1237 &self,
1238 requests: &[rustev_contract::run::RequestRecord],
1239 policy: &rustev_contract::optimization::DecisionCachePolicy,
1240 ) -> Result<Option<(RequestKey, ReusedRequests)>, String> {
1241 use rustev_contract::run::{RequestResult, Transition};
1242 let (Some(key), None) = (&self.key, &self.hit) else {
1243 return Ok(None);
1244 };
1245 let log = self.log();
1246 let mut reused = ReusedRequests::new();
1247 for r in requests.iter().filter(|r| !self.closure.contains(&r.step)) {
1248 let at = (r.step.clone(), r.instance.clone());
1249 let fallback = r
1250 .transitions
1251 .iter()
1252 .any(|t| matches!(t, Transition::Fallback { .. }));
1253 let source = r
1254 .supplied_from()
1255 .filter(|f| f.target == 0 && matches!(r.result, RequestResult::Output { .. }))
1256 .and_then(|f| {
1257 f.attempt
1258 .map(|a| a.attempt_id.clone())
1259 .or(f.reused_from.map(String::from))
1260 });
1261 match (fallback, source, log.keys.get(&at), log.outputs.get(&at)) {
1262 (false, Some(source_attempt_id), Some(k), Some(output)) => {
1263 reused.insert(
1264 at,
1265 ReusedRequest {
1266 key: k.clone(),
1267 output: output.clone(),
1268 source_attempt_id,
1269 },
1270 );
1271 }
1272 _ => {
1273 return Err(format!(
1274 "decision cache not admitted: step {:?} has no validated target 0 output",
1275 r.step
1276 ));
1277 }
1278 }
1279 }
1280 match decision_entry_bytes(key, &reused) {
1281 Ok(bytes) if bytes <= policy.max_entry_bytes => Ok(Some((key.clone(), reused))),
1282 Ok(_) | Err(CacheRefusal::EntryTooLarge) => {
1283 Err("decision cache not admitted: entry exceeds max_entry_bytes".into())
1284 }
1285 Err(_) => {
1286 Err("decision cache not admitted: an output has no canonical form".into())
1287 }
1288 }
1289 }
1290
1291 pub(crate) fn diagnose(&self, diagnostic: String) {
1293 self.log().diagnostics.push(diagnostic);
1294 }
1295 }
1296
1297 #[cfg(test)]
1298 mod tests {
1299 use rustev_contract::definition::ProvenanceClass;
1300 use rustev_contract::optimization::{
1301 DecisionCacheTier, ExpiryBasis, OptimizationAdmission, RefreshFailurePolicy,
1302 };
1303 use rustev_contract::snapshot::Entry;
1304
1305 use super::super::InvalidationSelector;
1306 use super::*;
1307
1308 fn plan() -> (PlanId, OptimizationPolicy) {
1309 (
1310 PlanId::parse(&format!("sha256:{}", "1".repeat(64))).unwrap(),
1311 OptimizationPolicy {
1312 key_schema_version: 1,
1313 cache_namespace_version: 1,
1314 admission: OptimizationAdmission::Refuse,
1315 expiry: ExpiryBasis::InjectedRuntimeTime,
1316 batch: None,
1317 memory_cache: None,
1318 shared_calls: None,
1319 persistent_cache: None,
1320 decision_cache: None,
1321 },
1322 )
1323 }
1324
1325 fn cache(max_entries: u64, max_entry_bytes: u64) -> DecisionCachePolicy {
1326 DecisionCachePolicy {
1327 tiers: vec![DecisionCacheTier::Memory],
1328 max_entry_bytes,
1329 max_entries: Some(max_entries),
1330 ttl_ms: 100,
1331 volatile: vec!["live".into()],
1332 key_dimensions: vec!["revision".into()],
1333 on_refresh_failure: RefreshFailurePolicy::Unresolved,
1334 }
1335 }
1336
1337 fn schema() -> Vec<String> {
1340 [
1341 "plan_id",
1342 "key_schema_version",
1343 "cache_namespace_version",
1344 "runtime_version",
1345 "scope",
1346 "profile",
1347 "dimension:revision",
1348 "input:stable",
1349 ]
1350 .map(String::from)
1351 .to_vec()
1352 }
1353
1354 fn snapshot(stable: i64, live: i64) -> Snapshot {
1355 let entry = |field: &str, value: i64| Entry {
1356 field: field.into(),
1357 value: value.into(),
1358 provenance: ProvenanceClass::SystemOfRecord,
1359 as_of_ms: Timestamp::from_ms(1).unwrap(),
1360 source: "SYNTHETIC".into(),
1361 grade: None,
1362 producer: None,
1363 };
1364 Snapshot {
1365 schema: rustev_contract::schema::SNAPSHOT.into(),
1366 entries: vec![entry("stable", stable), entry("live", live)],
1367 }
1368 }
1369
1370 fn dims(revision: &str) -> BTreeMap<String, Handle> {
1371 BTreeMap::from([("revision".to_string(), Handle::new(revision).unwrap())])
1372 }
1373
1374 fn key_with(
1375 schema: &[String],
1376 profile: &str,
1377 principal: &[u8],
1378 dimensions: &BTreeMap<String, Handle>,
1379 snapshot: &Snapshot,
1380 ) -> Result<RequestKey, Unkeyed> {
1381 let (plan_id, policy) = plan();
1382 decision_key(DecisionKeyParts {
1383 schema,
1384 plan_id: &plan_id,
1385 profile,
1386 policy: &policy,
1387 principal,
1388 dimensions,
1389 snapshot,
1390 })
1391 }
1392
1393 fn key(principal: &[u8]) -> RequestKey {
1394 key_with(&schema(), "none", principal, &dims("r1"), &snapshot(1, 1)).unwrap()
1395 }
1396
1397 fn at(ms: i64) -> Timestamp {
1398 Timestamp::from_ms(ms).unwrap()
1399 }
1400
1401 fn entry(created: i64, output_bytes: usize) -> DecisionEntry {
1403 let reused = ReusedRequest {
1404 key: key(b"request"),
1405 output: RawOutput::Label("x".repeat(output_bytes)),
1406 source_attempt_id: "d1/topic/0/1".into(),
1407 };
1408 DecisionEntry {
1409 requests: BTreeMap::from([(("topic".to_string(), vec![]), reused)]),
1410 source_run_record: ContentDigest::parse(&format!("sha256:{}", "2".repeat(64)))
1411 .unwrap(),
1412 created: at(created),
1413 }
1414 }
1415
1416 #[test]
1422 fn the_decision_key_follows_its_schema() {
1423 let (plan_id, policy) = plan();
1424 let s = schema();
1425 let base = key(b"tenant-a");
1426 assert_eq!(base.namespace, namespace(&plan_id, &policy));
1427 assert_eq!(base.scope_id, scope_id(b"tenant-a"));
1428 assert_eq!(
1431 key_with(&s, "none", b"tenant-a", &dims("r1"), &snapshot(1, 2)).unwrap(),
1432 base
1433 );
1434 for other in [
1435 key_with(&s, r#"["p"]"#, b"tenant-a", &dims("r1"), &snapshot(1, 1)),
1436 key_with(&s, "none", b"tenant-b", &dims("r1"), &snapshot(1, 1)),
1437 key_with(&s, "none", b"tenant-a", &dims("r2"), &snapshot(1, 1)),
1438 key_with(&s, "none", b"tenant-a", &dims("r1"), &snapshot(2, 1)),
1439 ] {
1440 assert_ne!(other.unwrap().id, base.id);
1441 }
1442 let missing = key_with(&s, "none", b"tenant-a", &BTreeMap::new(), &snapshot(1, 1));
1443 assert_eq!(
1444 missing.unwrap_err().diagnostic(),
1445 "key_dimension_missing: \"revision\""
1446 );
1447 let mut extra = dims("r1");
1448 extra.insert("region".into(), Handle::new("eu").unwrap());
1449 assert_eq!(
1450 key_with(&s, "none", b"tenant-a", &extra, &snapshot(1, 1))
1451 .unwrap_err()
1452 .diagnostic(),
1453 "key_ineligible: dimension \"region\" is not declared by the plan"
1454 );
1455 let mut unknown = s.clone();
1456 unknown.push("clock".into());
1457 assert_eq!(
1458 key_with(&unknown, "none", b"tenant-a", &dims("r1"), &snapshot(1, 1))
1459 .unwrap_err()
1460 .diagnostic(),
1461 "key_ineligible: unknown key member \"clock\""
1462 );
1463 }
1464
1465 #[test]
1469 fn a_decision_entry_expires_by_evaluation_time() {
1470 let o = Optimizer::default();
1471 let k = key(b"tenant-a");
1472 let id = o
1473 .insert_decision(&cache(4, 4_096), k.clone(), entry(1_000, 1), 0)
1474 .unwrap();
1475 let hit = o.lookup_decision(&k, at(1_005), 100).unwrap();
1476 assert_eq!((hit.entry_id.as_str(), hit.age_ms), (id.as_str(), 5));
1477 assert_eq!(hit.entry.requests.len(), 1);
1478 assert_eq!(o.lookup_decision(&k, at(1_100), 100).unwrap().age_ms, 100);
1479 assert_eq!(
1480 o.lookup_decision(&k, at(999), 100).unwrap_err(),
1481 DecisionMiss::FromFuture
1482 );
1483 assert!(o.lookup_decision(&k, at(1_000), 100).is_ok(), "kept");
1484 assert_eq!(
1485 o.lookup_decision(&k, at(1_101), 100).unwrap_err(),
1486 DecisionMiss::Expired
1487 );
1488 assert_eq!(
1489 o.lookup_decision(&k, at(1_000), 100).unwrap_err(),
1490 DecisionMiss::Absent,
1491 "removed"
1492 );
1493 assert_ne!(id, k.id);
1496 let codes = [
1497 DecisionMiss::Absent,
1498 DecisionMiss::Collision,
1499 DecisionMiss::GenerationAdvanced,
1500 DecisionMiss::FromFuture,
1501 DecisionMiss::Expired,
1502 ]
1503 .map(DecisionMiss::diagnostic);
1504 assert_eq!(
1505 codes,
1506 [
1507 "decision cache miss",
1508 "entry_collision",
1509 "generation_advanced",
1510 "entry_from_future",
1511 "entry_expired"
1512 ]
1513 );
1514 }
1515
1516 #[test]
1522 fn admission_is_bounded_and_checked() {
1523 let o = Optimizer::default();
1524 let a = key(b"tenant-a");
1525 let small = decision_entry_bytes(&a, &entry(0, 1).requests).unwrap();
1526 let policy = cache(2, small);
1527 assert_eq!(
1528 o.insert_decision(&policy, a.clone(), entry(0, 2), 0),
1529 Err(CacheRefusal::EntryTooLarge)
1530 );
1531 for max_entries in [None, Some(0)] {
1532 let empty = DecisionCachePolicy {
1533 max_entries,
1534 ..policy.clone()
1535 };
1536 assert_eq!(
1537 o.insert_decision(&empty, a.clone(), entry(0, 1), 0),
1538 Err(CacheRefusal::NoCapacity)
1539 );
1540 }
1541 o.insert_decision(&policy, a.clone(), entry(0, 1), 0)
1542 .unwrap();
1543 assert_eq!(
1544 o.insert_decision(&policy, a.clone(), entry(0, 1), 1),
1545 Err(CacheRefusal::StaleGeneration)
1546 );
1547 let mut other = a.clone();
1548 *other.material.last_mut().unwrap() ^= 1;
1549 assert_eq!(
1550 o.insert_decision(&policy, other.clone(), entry(0, 1), 0),
1551 Err(CacheRefusal::Collision)
1552 );
1553 assert_eq!(
1554 o.lookup_decision(&other, at(0), 100).unwrap_err(),
1555 DecisionMiss::Collision
1556 );
1557 o.insert_decision(&policy, a.clone(), entry(0, 1), 0)
1559 .unwrap();
1560 let (b, c) = (key(b"tenant-b"), key(b"tenant-c"));
1561 o.insert_decision(&policy, b.clone(), entry(0, 1), 0)
1562 .unwrap();
1563 o.insert_decision(&policy, c.clone(), entry(0, 1), 0)
1564 .unwrap();
1565 assert_eq!(
1566 o.lookup_decision(&a, at(0), 100).unwrap_err(),
1567 DecisionMiss::Absent
1568 );
1569 assert!(o.lookup_decision(&b, at(0), 100).is_ok());
1570 assert!(o.lookup_decision(&c, at(0), 100).is_ok());
1571 }
1572
1573 #[test]
1577 fn invalidation_reaches_decision_entries() {
1578 let o = Optimizer::default();
1579 let (a, b) = (key(b"tenant-a"), key(b"tenant-b"));
1580 let policy = cache(4, 4_096);
1581 o.insert_decision(&policy, a.clone(), entry(0, 1), 0)
1582 .unwrap();
1583 o.insert_decision(&policy, b.clone(), entry(0, 1), 0)
1584 .unwrap();
1585 let bytes = decision_entry_bytes(&a, &entry(0, 1).requests).unwrap();
1586 let result = o.invalidate(&InvalidationSelector::Scope {
1587 namespace: a.namespace.clone(),
1588 scope_id: a.scope_id.clone(),
1589 });
1590 assert_eq!((result.removed_entries, result.removed_bytes), (1, bytes));
1591 assert_eq!(
1592 o.lookup_decision(&a, at(0), 100).unwrap_err(),
1593 DecisionMiss::Absent
1594 );
1595 assert_eq!(
1596 o.lookup_decision(&b, at(0), 100).unwrap_err(),
1597 DecisionMiss::GenerationAdvanced
1598 );
1599 assert_eq!(
1600 o.insert_decision(&policy, a.clone(), entry(0, 1), 0),
1601 Err(CacheRefusal::StaleGeneration)
1602 );
1603 o.insert_decision(&policy, a.clone(), entry(0, 1), result.generation)
1604 .unwrap();
1605 let all = o.invalidate(&InvalidationSelector::Namespace {
1606 namespace: a.namespace.clone(),
1607 });
1608 assert_eq!(all.removed_entries, 1);
1609 }
1610 }
1611}
1612
1613#[cfg(test)]
1614mod tests {
1615 use std::collections::BTreeMap;
1616
1617 use rustev_contract::ids::{ArtifactId, DescriptorId, PlanId};
1618 use rustev_contract::optimization::{EvictionOrder, ExpiryBasis, OptimizationAdmission};
1619
1620 use super::*;
1621
1622 fn digest(byte: char) -> String {
1623 format!("sha256:{}", byte.to_string().repeat(64))
1624 }
1625
1626 fn policy(plan: char) -> (PlanId, OptimizationPolicy) {
1627 (
1628 PlanId::parse(&digest(plan)).unwrap(),
1629 OptimizationPolicy {
1630 key_schema_version: 1,
1631 cache_namespace_version: 1,
1632 admission: OptimizationAdmission::Refuse,
1633 expiry: ExpiryBasis::InjectedRuntimeTime,
1634 batch: None,
1635 memory_cache: Some(memory(4096, 2, 2048, 2048)),
1636 shared_calls: None,
1637 persistent_cache: None,
1638 decision_cache: None,
1639 },
1640 )
1641 }
1642
1643 fn memory(max_bytes: u64, max_entries: u64, entry: u64, scope: u64) -> MemoryCachePolicy {
1644 MemoryCachePolicy {
1645 acquisition_pages: false,
1646 max_bytes,
1647 max_entries,
1648 max_entry_bytes: entry,
1649 max_scope_bytes: scope,
1650 ttl_ms: 10,
1651 eviction: EvictionOrder::Fifo,
1652 }
1653 }
1654
1655 fn key_in(plan: char, principal: &[u8], step: &str) -> RequestKey {
1656 let (plan_id, policy) = policy(plan);
1657 request_key(KeyParts {
1658 plan_id: &plan_id,
1659 profile: "none",
1660 policy: &policy,
1661 principal,
1662 backend_artifact: &ArtifactId::parse(&digest('2')).unwrap(),
1663 backend_descriptor: &DescriptorId::parse(&digest('3')).unwrap(),
1664 step,
1665 instance: &["one".into()],
1666 projection: b"projection",
1667 })
1668 }
1669
1670 fn key(principal: &[u8]) -> RequestKey {
1671 key_in('1', principal, "classify")
1672 }
1673
1674 fn output(label: &str) -> RawOutput {
1675 RawOutput::Scores(BTreeMap::from([(label.into(), 1.0)]))
1676 }
1677
1678 fn write(attempt: &str, generation: u64) -> CacheWrite {
1679 CacheWrite {
1680 output: output(&"a".repeat(700)),
1681 source_attempt_id: attempt.into(),
1682 target: 0,
1683 captured_generation: generation,
1684 now: 0,
1685 }
1686 }
1687
1688 fn size(key: &RequestKey) -> u64 {
1690 let optimizer = Optimizer::default();
1691 let roomy = memory(1 << 20, 64, 1 << 20, 1 << 20);
1692 optimizer
1693 .insert(&roomy, key.clone(), write("x", 0))
1694 .unwrap();
1695 optimizer.total_bytes(&key.namespace)
1696 }
1697
1698 #[test]
1699 fn scope_and_instance_arity_are_part_of_the_complete_key() {
1700 let a = key(b"tenant-a");
1701 let b = key(b"tenant-b");
1702 assert_ne!(a.id, b.id);
1703 assert_ne!(a.scope_id, b.scope_id);
1704 assert_ne!(a.material, b.material);
1705 let (plan_id, policy) = policy('1');
1707 let with = |instance: &[String]| {
1708 request_key(KeyParts {
1709 plan_id: &plan_id,
1710 profile: "none",
1711 policy: &policy,
1712 principal: b"t",
1713 backend_artifact: &ArtifactId::parse(&digest('2')).unwrap(),
1714 backend_descriptor: &DescriptorId::parse(&digest('3')).unwrap(),
1715 step: "s",
1716 instance,
1717 projection: b"p",
1718 })
1719 .id
1720 };
1721 assert_ne!(with(&["ab".into()]), with(&["a".into(), "b".into()]));
1722 }
1723
1724 #[test]
1725 fn memory_cache_checks_expiry_collision_and_invalidation_generation() {
1726 let optimizer = Optimizer::default();
1727 let memory = policy('1').1.memory_cache.unwrap();
1728 let key = key(b"tenant");
1729 optimizer
1730 .insert(
1731 &memory,
1732 key.clone(),
1733 CacheWrite {
1734 output: output("a"),
1735 source_attempt_id: "attempt-1".into(),
1736 target: 0,
1737 captured_generation: 0,
1738 now: 5,
1739 },
1740 )
1741 .unwrap();
1742 assert_eq!(optimizer.lookup(&key, 14).unwrap().age_ms, 9);
1743 assert_eq!(optimizer.lookup(&key, 15), Err(CacheMiss::Expired));
1744 assert_eq!(optimizer.lookup(&key, 15), Err(CacheMiss::Absent));
1745
1746 optimizer
1747 .insert(&memory, key.clone(), write("a-2", 0))
1748 .unwrap();
1749 optimizer.force_collision(&key, b"other material".to_vec());
1750 assert_eq!(optimizer.lookup(&key, 1), Err(CacheMiss::Collision));
1751 assert_eq!(
1752 optimizer.insert(&memory, key.clone(), write("a-3", 0)),
1753 Err(CacheRefusal::Collision)
1754 );
1755
1756 let result = optimizer.invalidate(&InvalidationSelector::Namespace {
1757 namespace: key.namespace.clone(),
1758 });
1759 assert_eq!(result.generation, 1);
1760 assert_eq!(result.removed_entries, 1);
1761 assert_eq!(
1764 optimizer.insert(&memory, key.clone(), write("late", 0)),
1765 Err(CacheRefusal::StaleGeneration)
1766 );
1767 assert_eq!(optimizer.lookup(&key, 1), Err(CacheMiss::Absent));
1768 optimizer
1769 .insert(&memory, key.clone(), write("fresh", 1))
1770 .unwrap();
1771 assert_eq!(
1772 optimizer.lookup(&key, 1).unwrap().source_attempt_id,
1773 "fresh"
1774 );
1775 }
1776
1777 #[test]
1778 fn an_oversized_entry_is_refused_before_anything_is_evicted() {
1779 let optimizer = Optimizer::default();
1780 let a = key_in('1', b"tenant", "a");
1781 let n = size(&a);
1782 let tight = memory(4 * n, 2, n, 2 * n);
1783 optimizer.insert(&tight, a.clone(), write("a", 0)).unwrap();
1784 let big = CacheWrite {
1785 output: output(&"b".repeat(800)),
1786 ..write("b", 0)
1787 };
1788 assert_eq!(
1789 optimizer.insert(&tight, key_in('1', b"tenant", "b"), big),
1790 Err(CacheRefusal::EntryTooLarge)
1791 );
1792 assert!(optimizer.lookup(&a, 0).is_ok(), "nothing was evicted");
1793 }
1794
1795 #[test]
1796 fn total_and_count_bounds_evict_the_namespace_fifo_head() {
1797 let optimizer = Optimizer::default();
1798 let a = key(b"tenant-a");
1799 let n = size(&a);
1800 let total = memory(2 * n + n / 2, 8, n, 2 * n + n / 2);
1801 let b = key(b"tenant-b");
1802 let c = key(b"tenant-c");
1803 optimizer.insert(&total, a.clone(), write("a", 0)).unwrap();
1804 optimizer.insert(&total, b.clone(), write("b", 0)).unwrap();
1805 let admission = optimizer.insert(&total, c.clone(), write("c", 0)).unwrap();
1806 assert_eq!(admission.evicted_entries, 1);
1807 assert_eq!(optimizer.lookup(&a, 0), Err(CacheMiss::Absent));
1808 assert!(optimizer.lookup(&b, 0).is_ok());
1809 assert!(optimizer.lookup(&c, 0).is_ok());
1810
1811 let optimizer = Optimizer::default();
1812 let count = memory(100 * n, 2, n, 100 * n);
1813 optimizer.insert(&count, a.clone(), write("a", 0)).unwrap();
1814 optimizer.insert(&count, b.clone(), write("b", 0)).unwrap();
1815 let admission = optimizer.insert(&count, c.clone(), write("c", 0)).unwrap();
1816 assert_eq!(admission.evicted_entries, 1);
1817 assert_eq!(optimizer.lookup(&a, 0), Err(CacheMiss::Absent));
1818 }
1819
1820 #[test]
1821 fn a_scope_bound_evicts_only_that_scopes_entries() {
1822 let optimizer = Optimizer::default();
1823 let b = key_in('1', b"tenant-b", "b");
1825 let n = size(&b);
1826 let scoped = memory(100 * n, 64, n + 8, 2 * n + n / 2);
1828 optimizer.insert(&scoped, b.clone(), write("b", 0)).unwrap();
1829 let a1 = key_in('1', b"tenant-a", "a1");
1830 let a2 = key_in('1', b"tenant-a", "a2");
1831 let a3 = key_in('1', b"tenant-a", "a3");
1832 optimizer
1833 .insert(&scoped, a1.clone(), write("a1", 0))
1834 .unwrap();
1835 optimizer
1836 .insert(&scoped, a2.clone(), write("a2", 0))
1837 .unwrap();
1838 let admission = optimizer
1839 .insert(&scoped, a3.clone(), write("a3", 0))
1840 .unwrap();
1841 assert_eq!(admission.evicted_entries, 1);
1842 assert_eq!(optimizer.lookup(&a1, 0), Err(CacheMiss::Absent));
1843 assert!(optimizer.lookup(&a2, 0).is_ok());
1844 assert!(optimizer.lookup(&a3, 0).is_ok());
1845 assert!(
1846 optimizer.lookup(&b, 0).is_ok(),
1847 "tenant A's traffic never evicts tenant B"
1848 );
1849 }
1850
1851 #[test]
1852 fn one_plans_bounds_never_evict_another_plans_entries() {
1853 let optimizer = Optimizer::default();
1854 let roomy = memory(100_000, 64, 2048, 100_000);
1855 let tiny = memory(2048, 1, 2048, 2048);
1856 let other = key_in('7', b"tenant", "x");
1857 optimizer
1858 .insert(&roomy, other.clone(), write("x", 0))
1859 .unwrap();
1860 for step in ["a", "b", "c"] {
1861 optimizer
1862 .insert(&tiny, key_in('1', b"tenant", step), write(step, 0))
1863 .unwrap();
1864 }
1865 assert!(optimizer.lookup(&other, 0).is_ok());
1866 optimizer.invalidate(&InvalidationSelector::Namespace {
1868 namespace: key_in('1', b"tenant", "a").namespace,
1869 });
1870 assert_eq!(optimizer.generation(&other.namespace), 0);
1871 assert!(optimizer.lookup(&other, 0).is_ok());
1872 }
1873
1874 #[test]
1875 fn scope_invalidation_is_exact() {
1876 let optimizer = Optimizer::default();
1877 let roomy = memory(100_000, 64, 2048, 100_000);
1878 let a = key(b"tenant-a");
1879 let b = key(b"tenant-b");
1880 optimizer.insert(&roomy, a.clone(), write("a", 0)).unwrap();
1881 optimizer.insert(&roomy, b.clone(), write("b", 0)).unwrap();
1882 let result = optimizer.invalidate(&InvalidationSelector::Scope {
1883 namespace: a.namespace.clone(),
1884 scope_id: a.scope_id.clone(),
1885 });
1886 assert_eq!(result.removed_entries, 1);
1887 assert_eq!(optimizer.lookup(&a, 0), Err(CacheMiss::Absent));
1888 assert_eq!(optimizer.lookup(&b, 0), Err(CacheMiss::Expired));
1891 }
1892
1893 #[test]
1894 fn lazily_deleted_queue_slots_stay_bounded() {
1895 let optimizer = Optimizer::default();
1896 let roomy = memory(100_000, 64, 2048, 100_000);
1897 let key = key(b"tenant");
1898 for n in 0..500 {
1899 optimizer
1900 .insert(&roomy, key.clone(), write(&format!("a{n}"), 0))
1901 .unwrap();
1902 }
1903 let (fifo, scoped) = optimizer.queue_lengths(&key.namespace);
1904 assert!(fifo <= 20 && scoped <= 20, "{fifo} {scoped}");
1905 }
1906
1907 #[test]
1910 fn the_profile_is_a_key_member() {
1911 let (plan_id, policy) = policy('1');
1912 let with = |profile: &str| {
1913 request_key(KeyParts {
1914 plan_id: &plan_id,
1915 profile,
1916 policy: &policy,
1917 principal: b"tenant-a",
1918 backend_artifact: &ArtifactId::parse(&digest('2')).unwrap(),
1919 backend_descriptor: &DescriptorId::parse(&digest('3')).unwrap(),
1920 step: "classify",
1921 instance: &["one".into()],
1922 projection: b"projection",
1923 })
1924 };
1925 let none = with("none");
1926 let a = with(&digest('a'));
1927 let b = with(&digest('b'));
1928 assert_eq!(none, key_in('1', b"tenant-a", "classify"));
1929 assert_ne!(none.id, a.id);
1930 assert_ne!(a.id, b.id);
1931 assert_eq!(profile_ids(&["a", "b"]), r#"["a","b"]"#);
1933 assert_ne!(profile_ids(&["a,b"]), profile_ids(&["a", "b"]));
1934 assert_ne!(
1935 with(&profile_ids(&["a,b"])).id,
1936 with(&profile_ids(&["a", "b"])).id
1937 );
1938 assert_eq!(
1940 (&a.namespace, &a.scope_id),
1941 (&none.namespace, &none.scope_id)
1942 );
1943 }
1944}