Skip to main content

rustev_runtime/
store.rs

1//! The host's store behind the persistent tier (spec 044, 3.4).
2//!
3//! [`RuntimeStore`] is the seam: the store keeps bytes under a namespace
4//! and key id, with a namespace and a scope generation, and never
5//! interprets an entry. Everything that makes an entry servable is checked
6//! here, on every read, in the order of 3.4.3; any failure is a miss with
7//! its diagnostic, never a value.
8
9use std::collections::{BTreeMap, BTreeSet};
10use std::sync::Mutex;
11
12use rustev_contract::Document;
13use rustev_contract::cache_entry::{
14    CacheEntry, DecisionPayload, EntryBody, EntryGenerations, EntryKind, EntryPayload,
15    RequestPayload, ReusedRequestPayload,
16};
17use rustev_contract::canonical::CanonicalError;
18use rustev_contract::ids::ContentDigest;
19use rustev_contract::optimization::{OptimizationPolicy, PersistentCachePolicy, StoreContract};
20use rustev_contract::output::RawOutput;
21use rustev_contract::schema;
22use rustev_contract::time::DurationMs;
23use rustev_contract::time::Timestamp;
24use rustev_core::seams::{BoxFuture, CancelSignal};
25
26use crate::Inner;
27use crate::clock::Instant;
28use crate::optimization::RequestKey;
29use crate::optimization::decision::{DecisionEntry, ReusedRequest, ReusedRequests};
30
31/// What a store call made for a decision receives (3.4.1): the decision's
32/// remaining time and a signal raised on its cancellation or deadline.
33#[derive(Debug, Clone)]
34pub struct StoreCall {
35    /// Milliseconds left before the decision's deadline, or none.
36    pub remaining_ms: Option<u64>,
37    pub cancel: CancelSignal,
38}
39
40/// The bytes under a key, if any, with the generations read atomically
41/// with them.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct Stored {
44    pub bytes: Option<Vec<u8>>,
45    pub generations: EntryGenerations,
46}
47
48/// The conditions of one `put` (3.4.1 and 3.4.4).
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct PutTerms {
51    /// The scope the entry belongs to, as the runtime identifies it.
52    pub scope: String,
53    /// Store only if these still hold.
54    pub generations: EntryGenerations,
55    pub max_scope_bytes: u64,
56    pub max_namespace_bytes: u64,
57    /// The evaluation time after which the entry is expired, a hint for
58    /// reclamation (3.4.3).
59    pub expires: Timestamp,
60}
61
62/// How a `put` ended. Every answer but `Stored` is an admission not made.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum PutAnswer {
65    Stored,
66    /// A generation moved since the work began; nothing was written.
67    GenerationAdvanced,
68    /// A scope or namespace budget would be exceeded.
69    OverBudget,
70    /// Another writer stored an entry for the key first; the store kept
71    /// the one its contract's `conflict` term names.
72    Conflict,
73}
74
75impl PutAnswer {
76    /// The diagnostic of a refused admission (3.9).
77    pub fn diagnostic(self) -> Option<&'static str> {
78        match self {
79            PutAnswer::Stored => None,
80            PutAnswer::GenerationAdvanced => Some("generation_advanced"),
81            PutAnswer::OverBudget => Some("over_budget"),
82            PutAnswer::Conflict => Some("store_conflict"),
83        }
84    }
85}
86
87/// Which entries an invalidation covers.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum StoreSelector {
90    Namespace,
91    Scope(String),
92}
93
94/// The selected generation after an invalidation, and the entries it
95/// removed.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct Invalidated {
98    pub generation: u64,
99    pub removed: u64,
100}
101
102/// A store failure, timeout or cancellation (3.4.1). It never fails an
103/// inference that already happened.
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct StoreFailure {
106    pub detail: String,
107}
108
109/// A host-implemented store (3.4.1). Every call is polled on the task of
110/// the decision that made it.
111pub trait RuntimeStore: Send + Sync {
112    /// The store's contract; `prepare` compares it with the plan's.
113    fn contract(&self) -> &StoreContract;
114
115    /// The entry under `key_id`, if any, and the namespace and `scope`
116    /// generations, read atomically.
117    fn get<'a>(
118        &'a self,
119        namespace: &'a str,
120        scope: &'a str,
121        key_id: &'a str,
122        call: StoreCall,
123    ) -> BoxFuture<'a, Result<Stored, StoreFailure>>;
124
125    /// Store `bytes` under `key_id` if `terms` still hold.
126    fn put<'a>(
127        &'a self,
128        namespace: &'a str,
129        key_id: &'a str,
130        bytes: Vec<u8>,
131        terms: PutTerms,
132        call: StoreCall,
133    ) -> BoxFuture<'a, Result<PutAnswer, StoreFailure>>;
134
135    /// Remove one entry.
136    fn delete<'a>(
137        &'a self,
138        namespace: &'a str,
139        key_id: &'a str,
140        call: StoreCall,
141    ) -> BoxFuture<'a, Result<(), StoreFailure>>;
142
143    /// Atomically advance the selected generation and make the entries it
144    /// covers unreadable.
145    fn invalidate<'a>(
146        &'a self,
147        namespace: &'a str,
148        selector: StoreSelector,
149        call: StoreCall,
150    ) -> BoxFuture<'a, Result<Invalidated, StoreFailure>>;
151}
152
153/// Why a stored entry is not served (3.4.3), in the order checked.
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub(crate) enum EntryMiss {
156    /// Not a `rustev.cache-entry/1` document.
157    SchemaUnknown,
158    /// Not JSON, its integrity digest does not match, or its key id is not
159    /// the one its key material names.
160    Corrupt,
161    /// Retained key material other than the request's: equal key ids over
162    /// different material (spec 017, 3.2), or an entry the store returned
163    /// for another key.
164    Collision,
165    /// The store's generations moved past the entry's.
166    GenerationAdvanced,
167    Expired,
168    /// Written at a later evaluation time than the request's.
169    FromFuture,
170}
171
172impl EntryMiss {
173    /// The diagnostic recorded with the miss: a 3.9 code, or for a
174    /// collision and an expiry, which 3.9 does not name, a code of the
175    /// same form.
176    pub(crate) fn diagnostic(self) -> &'static str {
177        match self {
178            EntryMiss::SchemaUnknown => "entry_schema_unknown",
179            EntryMiss::Corrupt => "entry_corrupt",
180            EntryMiss::Collision => "entry_collision",
181            EntryMiss::GenerationAdvanced => "generation_advanced",
182            EntryMiss::Expired => "entry_expired",
183            EntryMiss::FromFuture => "entry_from_future",
184        }
185    }
186
187    /// Only a corrupt entry is deleted; expiry and generations are the
188    /// store's to reclaim (section 5).
189    pub(crate) fn deletes(self) -> bool {
190        self == EntryMiss::Corrupt
191    }
192}
193
194/// What a read is checked against.
195pub(crate) struct Expected<'a> {
196    pub kind: EntryKind,
197    pub key: &'a RequestKey,
198    /// The store's current generations, read with the entry.
199    pub generations: EntryGenerations,
200    /// The request's evaluation time.
201    pub now: Timestamp,
202}
203
204/// A served payload and the entry's age at the request's evaluation time.
205#[derive(Debug, Clone, PartialEq)]
206pub(crate) struct Served {
207    pub payload: EntryPayload,
208    pub created: Timestamp,
209    pub age_ms: u64,
210    pub evidence: ContentDigest,
211}
212
213/// Check `bytes` in the order of 3.4.3. The core's validation of the
214/// output, the last check, is the caller's.
215pub(crate) fn read_entry(bytes: &[u8], e: Expected<'_>) -> Result<Served, EntryMiss> {
216    // Bytes that are not JSON at all are corrupt. A JSON document that does
217    // not name this schema is another format the store holds: unknown,
218    // never deleted.
219    let doc = serde_json::from_slice::<serde_json::Value>(bytes).map_err(|_| EntryMiss::Corrupt)?;
220    if doc.get("schema").and_then(|s| s.as_str()) != Some(schema::CACHE_ENTRY) {
221        return Err(EntryMiss::SchemaUnknown);
222    }
223    let entry = CacheEntry::parse(bytes).map_err(|_| EntryMiss::Corrupt)?;
224    if !entry.intact() {
225        return Err(EntryMiss::Corrupt);
226    }
227    let body = entry.entry;
228    // A payload of another kind than the entry names was written wrong.
229    if !matches!(
230        (body.kind, &body.payload),
231        (EntryKind::Request, EntryPayload::Request(_))
232            | (EntryKind::Decision, EntryPayload::Decision(_))
233    ) {
234        return Err(EntryMiss::Corrupt);
235    }
236    // An intact entry of the other kind is another key's.
237    if body.kind != e.kind || body.key_material != hex(&e.key.material) {
238        return Err(EntryMiss::Collision);
239    }
240    // Equal material names one key id; an entry that pairs them otherwise
241    // was written wrong, whatever its digest.
242    if body.key_id != e.key.id {
243        return Err(EntryMiss::Corrupt);
244    }
245    if body.generations != e.generations {
246        return Err(EntryMiss::GenerationAdvanced);
247    }
248    let (now, created) = (e.now.as_ms(), body.created.as_ms());
249    if now < created {
250        return Err(EntryMiss::FromFuture);
251    }
252    let age_ms = u64::try_from(now - created).unwrap_or(u64::MAX);
253    if age_ms > body.ttl_ms {
254        return Err(EntryMiss::Expired);
255    }
256    Ok(Served {
257        payload: body.payload,
258        created: body.created,
259        age_ms,
260        evidence: body.evidence,
261    })
262}
263
264/// What an entry is written from; its kind is its payload's.
265pub(crate) struct EntryWrite<'a> {
266    pub key: &'a RequestKey,
267    pub policy: &'a OptimizationPolicy,
268    pub generations: EntryGenerations,
269    pub created: Timestamp,
270    pub ttl_ms: u64,
271    pub payload: EntryPayload,
272    pub evidence: ContentDigest,
273}
274
275/// The record canonical bytes of a sealed entry.
276pub(crate) fn write_entry(w: EntryWrite<'_>) -> Result<Vec<u8>, CanonicalError> {
277    let entry = CacheEntry::seal(EntryBody {
278        kind: match w.payload {
279            EntryPayload::Request(_) => EntryKind::Request,
280            EntryPayload::Decision(_) => EntryKind::Decision,
281        },
282        key_material: hex(&w.key.material),
283        key_id: w.key.id.clone(),
284        key_schema_version: w.policy.key_schema_version,
285        namespace_version: w.policy.cache_namespace_version,
286        runtime_version: env!("CARGO_PKG_VERSION").into(),
287        generations: w.generations,
288        created: w.created,
289        ttl_ms: w.ttl_ms,
290        payload: w.payload,
291        evidence: w.evidence,
292    })?;
293    entry.record_canonical()
294}
295
296/// A validated output of a request's primary target, queued for the
297/// persistent tier until the decision's evidence exists.
298pub(crate) struct PendingWrite {
299    pub step: String,
300    pub instance: Vec<String>,
301    pub key: RequestKey,
302    pub output: RawOutput,
303    pub source_attempt_id: String,
304}
305
306/// One decision's persistent-tier state: the generations each read
307/// captured before the work began, by key id, and the writes queued.
308#[derive(Default)]
309pub(crate) struct Pending {
310    captured: Mutex<BTreeMap<String, EntryGenerations>>,
311    writes: Mutex<Vec<PendingWrite>>,
312}
313
314impl Pending {
315    /// Keep the first generations read for a key: those before the work.
316    pub(crate) fn capture(&self, key_id: &str, generations: EntryGenerations) {
317        lock(&self.captured)
318            .entry(key_id.to_string())
319            .or_insert(generations);
320    }
321
322    pub(crate) fn queue(&self, write: PendingWrite) {
323        lock(&self.writes).push(write);
324    }
325}
326
327fn lock<S>(m: &Mutex<S>) -> std::sync::MutexGuard<'_, S> {
328    m.lock().unwrap_or_else(|e| e.into_inner())
329}
330
331/// What the decision's writes answered, by request: the diagnostic of each
332/// admission not made (3.4.1).
333pub(crate) type WriteDiagnostics = Vec<((String, Vec<String>), String)>;
334
335/// What the decision-end writes need besides the queue.
336pub(crate) struct Writing<'a> {
337    pub rt: &'a Inner,
338    pub store: &'a dyn RuntimeStore,
339    pub policy: &'a OptimizationPolicy,
340    pub persistent: &'a PersistentCachePolicy,
341    pub cancel: &'a CancelSignal,
342    pub deadline: Instant,
343    pub created: Timestamp,
344    /// The digest of the decision's evidence record (3.4.3), or none when
345    /// the record has no canonical form, so no entry can bind it.
346    pub evidence: Option<ContentDigest>,
347}
348
349/// What a decision's store calls are made under: the store, the
350/// persistent tier's policy, and the decision's cancellation and
351/// deadline (3.4.2).
352pub(crate) struct StoreCtx<'a> {
353    pub rt: &'a Inner,
354    pub store: &'a dyn RuntimeStore,
355    pub persistent: &'a PersistentCachePolicy,
356    pub cancel: &'a CancelSignal,
357    pub deadline: Instant,
358}
359
360impl Writing<'_> {
361    fn calls(&self) -> StoreCtx<'_> {
362        StoreCtx {
363            rt: self.rt,
364            store: self.store,
365            persistent: self.persistent,
366            cancel: self.cancel,
367            deadline: self.deadline,
368        }
369    }
370}
371
372/// Seal and `put` one entry under the generations its read captured
373/// (3.4.3). A selection awaiting its store invalidation, an entry over
374/// `max_entry_bytes` (3.4.4), every answer but `stored` and a failure are
375/// an admission not made, answered with its diagnostic.
376async fn put_entry(p: &StoreCtx<'_>, e: EntryWrite<'_>) -> Result<(), String> {
377    let key = e.key;
378    if p.rt.persistent_unavailable(&key.namespace, &key.scope_id) {
379        return Err(crate::driver::UNAVAILABLE.into());
380    }
381    let (created, ttl_ms, generations) = (e.created, e.ttl_ms, e.generations);
382    let bytes =
383        write_entry(e).map_err(|_| "persistent cache entry has no canonical form".to_string())?;
384    if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > p.persistent.max_entry_bytes {
385        return Err("over_budget: entry exceeds max_entry_bytes".into());
386    }
387    // A hint only: reads judge expiry from `created` and `ttl_ms`. Past
388    // the last representable time, the hint is that time.
389    let expires = created.checked_add(DurationMs(ttl_ms)).unwrap_or_else(|_| {
390        Timestamp::from_ms(rustev_contract::time::MAX_TIMESTAMP_MS)
391            .expect("the last representable time is a timestamp")
392    });
393    let terms = PutTerms {
394        scope: key.scope_id.clone(),
395        generations,
396        max_scope_bytes: p.persistent.max_scope_bytes,
397        max_namespace_bytes: p.persistent.max_namespace_bytes,
398        expires,
399    };
400    let answer = crate::driver::store_call(p.rt, p.cancel, p.deadline, |call| {
401        p.store.put(&key.namespace, &key.id, bytes, terms, call)
402    })
403    .await;
404    match answer {
405        Ok(answer) => answer.diagnostic().map_or(Ok(()), |d| Err(d.into())),
406        Err(failure) => Err(format!("store_failed: {}", failure.detail)),
407    }
408}
409
410/// Write each queued entry once, under the generations its read captured
411/// (3.4.3); a request whose store was never read has none and is not
412/// written. Every answer but `stored`, a failure and an entry over
413/// `max_entry_bytes` is a diagnostic for its request; none changes its
414/// output or the judgment.
415pub(crate) async fn write_pending(pending: Pending, w: Writing<'_>) -> WriteDiagnostics {
416    let captured = pending
417        .captured
418        .into_inner()
419        .unwrap_or_else(|e| e.into_inner());
420    let writes = pending
421        .writes
422        .into_inner()
423        .unwrap_or_else(|e| e.into_inner());
424    let mut written = BTreeSet::new();
425    let mut diagnostics = vec![];
426    for write in writes {
427        let Some(&generations) = captured.get(&write.key.id) else {
428            continue;
429        };
430        if !written.insert(write.key.id.clone()) {
431            continue;
432        }
433        let at = (write.step.clone(), write.instance.clone());
434        let Some(evidence) = w.evidence.clone() else {
435            diagnostics.push((
436                at,
437                "persistent cache entry not written: the evidence has no canonical form".into(),
438            ));
439            continue;
440        };
441        let entry = EntryWrite {
442            key: &write.key,
443            policy: w.policy,
444            generations,
445            created: w.created,
446            ttl_ms: w.persistent.ttl_ms,
447            payload: EntryPayload::Request(RequestPayload {
448                target: 0,
449                output: write.output,
450                source_attempt_id: write.source_attempt_id,
451            }),
452            evidence,
453        };
454        if let Err(d) = put_entry(&w.calls(), entry).await {
455            diagnostics.push((at, d));
456        }
457    }
458    diagnostics
459}
460
461/// What a decision's persistent read found (spec 044, 3.3 and 3.6.3).
462pub(crate) enum DecisionRead {
463    /// An entry that passed every check of 3.4.3, with the generations
464    /// read with it. Each reused output is validated when it is supplied
465    /// (3.6.3 item 2).
466    Hit {
467        entry: DecisionEntry,
468        age_ms: u64,
469        generations: EntryGenerations,
470    },
471    /// A miss with its diagnostic. The store's generations are captured
472    /// whenever it answered, for the decision's own write.
473    Miss {
474        captured: Option<EntryGenerations>,
475        diagnostic: String,
476    },
477    /// A store failure, timeout or cancellation (3.4.2).
478    Failed(String),
479}
480
481/// Read and check a decision key's entry in the order of 3.4.3; only a
482/// corrupt entry is deleted. A selection awaiting its store invalidation
483/// is not read and captures nothing (3.10.3).
484pub(crate) async fn read_decision(
485    p: &StoreCtx<'_>,
486    key: &RequestKey,
487    now: Timestamp,
488) -> DecisionRead {
489    if p.rt.persistent_unavailable(&key.namespace, &key.scope_id) {
490        return DecisionRead::Miss {
491            captured: None,
492            diagnostic: crate::driver::UNAVAILABLE.into(),
493        };
494    }
495    let stored = crate::driver::store_call(p.rt, p.cancel, p.deadline, |call| {
496        p.store.get(&key.namespace, &key.scope_id, &key.id, call)
497    })
498    .await;
499    let stored = match stored {
500        Ok(stored) => stored,
501        Err(failure) => return DecisionRead::Failed(format!("store_failed: {}", failure.detail)),
502    };
503    let captured = Some(stored.generations);
504    let Some(bytes) = stored.bytes else {
505        return DecisionRead::Miss {
506            captured,
507            diagnostic: "persistent decision cache miss".into(),
508        };
509    };
510    let read = read_entry(
511        &bytes,
512        Expected {
513            kind: EntryKind::Decision,
514            key,
515            generations: stored.generations,
516            now,
517        },
518    );
519    let miss = match read {
520        Ok(Served {
521            payload: EntryPayload::Decision(payload),
522            created,
523            age_ms,
524            ..
525        }) => match decision_entry(key, payload, created) {
526            Some(entry) => {
527                return DecisionRead::Hit {
528                    entry,
529                    age_ms,
530                    generations: stored.generations,
531                };
532            }
533            None => EntryMiss::Corrupt,
534        },
535        // The read checked the kind.
536        Ok(_) => EntryMiss::Collision,
537        Err(miss) => miss,
538    };
539    let mut diagnostic = miss.diagnostic().to_string();
540    if miss.deletes()
541        && let Err(failure) = crate::driver::store_call(p.rt, p.cancel, p.deadline, |call| {
542            p.store.delete(&key.namespace, &key.id, call)
543        })
544        .await
545    {
546        diagnostic = format!("{diagnostic}; store_failed: {}", failure.detail);
547    }
548    DecisionRead::Miss {
549        captured,
550        diagnostic,
551    }
552}
553
554/// A decision payload as the memory tier holds it. Key material that is
555/// not hexadecimal, or a request named twice, was written wrong: `None`.
556fn decision_entry(
557    key: &RequestKey,
558    payload: DecisionPayload,
559    created: Timestamp,
560) -> Option<DecisionEntry> {
561    let mut requests = ReusedRequests::new();
562    for r in payload.requests {
563        let reused = ReusedRequest {
564            key: RequestKey::from_material(
565                unhex(&r.key_material)?,
566                key.scope_id.clone(),
567                key.namespace.clone(),
568            ),
569            output: r.output,
570            source_attempt_id: r.source_attempt_id,
571        };
572        if requests.insert((r.step, r.instance), reused).is_some() {
573            return None;
574        }
575    }
576    Some(DecisionEntry {
577        requests,
578        source_run_record: payload.run_record,
579        created,
580    })
581}
582
583/// What a judged miss's decision entry is written from (spec 044, 3.6.4).
584pub(crate) struct DecisionWrite<'a> {
585    pub key: &'a RequestKey,
586    pub policy: &'a OptimizationPolicy,
587    pub requests: &'a ReusedRequests,
588    /// The decision's snapshot entries of its volatile inputs.
589    pub volatile: Vec<rustev_contract::snapshot::Entry>,
590    pub judgment: ContentDigest,
591    pub run_record: ContentDigest,
592    pub evidence: ContentDigest,
593    /// Captured by the decision's read, before the work began.
594    pub generations: EntryGenerations,
595    pub created: Timestamp,
596    /// The decision cache's `ttl_ms`.
597    pub ttl_ms: u64,
598}
599
600/// Write a decision entry once; `Err` is the diagnostic of an admission
601/// not made (3.4.1 and 3.4.4).
602pub(crate) async fn write_decision(p: &StoreCtx<'_>, w: DecisionWrite<'_>) -> Result<(), String> {
603    let requests = w
604        .requests
605        .iter()
606        .map(|((step, instance), r)| ReusedRequestPayload {
607            step: step.clone(),
608            instance: instance.clone(),
609            key_material: hex(&r.key.material),
610            output: r.output.clone(),
611            source_attempt_id: r.source_attempt_id.clone(),
612        })
613        .collect();
614    let entry = EntryWrite {
615        key: w.key,
616        policy: w.policy,
617        generations: w.generations,
618        created: w.created,
619        ttl_ms: w.ttl_ms,
620        payload: EntryPayload::Decision(DecisionPayload {
621            requests,
622            volatile: w.volatile,
623            judgment: w.judgment,
624            run_record: w.run_record,
625        }),
626        evidence: w.evidence,
627    };
628    put_entry(p, entry).await
629}
630
631fn unhex(s: &str) -> Option<Vec<u8>> {
632    let digit = |c: u8| match c {
633        b'0'..=b'9' => Some(c - b'0'),
634        b'a'..=b'f' => Some(c - b'a' + 10),
635        _ => None,
636    };
637    let s = s.as_bytes();
638    if !s.len().is_multiple_of(2) {
639        return None;
640    }
641    s.chunks(2)
642        .map(|pair| Some(digit(pair[0])? << 4 | digit(pair[1])?))
643        .collect()
644}
645
646fn hex(bytes: &[u8]) -> String {
647    const DIGITS: &[u8; 16] = b"0123456789abcdef";
648    let mut s = String::with_capacity(bytes.len() * 2);
649    for b in bytes {
650        s.push(DIGITS[usize::from(b >> 4)] as char);
651        s.push(DIGITS[usize::from(b & 0xf)] as char);
652    }
653    s
654}
655
656#[cfg(test)]
657mod tests {
658    use super::*;
659    use rustev_contract::canonical::record_canonical_bytes;
660    use rustev_contract::ids::{ArtifactId, DescriptorId, PlanId};
661    use rustev_contract::optimization::{ExpiryBasis, OptimizationAdmission};
662    use rustev_contract::output::RawOutput;
663
664    use crate::optimization::{KeyParts, request_key};
665
666    fn digest(c: char) -> String {
667        format!("sha256:{}", c.to_string().repeat(64))
668    }
669
670    fn policy() -> OptimizationPolicy {
671        OptimizationPolicy {
672            key_schema_version: 1,
673            cache_namespace_version: 1,
674            admission: OptimizationAdmission::Refuse,
675            expiry: ExpiryBasis::InjectedRuntimeTime,
676            batch: None,
677            memory_cache: None,
678            shared_calls: None,
679            persistent_cache: None,
680            decision_cache: None,
681        }
682    }
683
684    fn key(principal: &[u8]) -> RequestKey {
685        let plan = PlanId::parse(&digest('a')).unwrap();
686        let artifact = ArtifactId::parse(&digest('b')).unwrap();
687        let descriptor = DescriptorId::parse(&digest('c')).unwrap();
688        request_key(KeyParts {
689            plan_id: &plan,
690            profile: "none",
691            policy: &policy(),
692            principal,
693            backend_artifact: &artifact,
694            backend_descriptor: &descriptor,
695            step: "topic",
696            instance: &[],
697            projection: b"{}",
698        })
699    }
700
701    const GENS: EntryGenerations = EntryGenerations {
702        namespace: 3,
703        scope: 7,
704    };
705
706    fn at(ms: i64) -> Timestamp {
707        Timestamp::from_ms(ms).unwrap()
708    }
709
710    fn entry(k: &RequestKey) -> Vec<u8> {
711        write_entry(EntryWrite {
712            key: k,
713            policy: &policy(),
714            generations: GENS,
715            created: at(1_000),
716            ttl_ms: 500,
717            payload: EntryPayload::Request(RequestPayload {
718                target: 0,
719                output: RawOutput::Label("billing".into()),
720                source_attempt_id: "att-1".into(),
721            }),
722            evidence: ContentDigest::parse(&digest('d')).unwrap(),
723        })
724        .unwrap()
725    }
726
727    fn read(
728        bytes: &[u8],
729        k: &RequestKey,
730        gens: EntryGenerations,
731        now: i64,
732    ) -> Result<u64, EntryMiss> {
733        read_entry(
734            bytes,
735            Expected {
736                kind: EntryKind::Request,
737                key: k,
738                generations: gens,
739                now: at(now),
740            },
741        )
742        .map(|s| s.age_ms)
743    }
744
745    #[test]
746    fn an_intact_current_unexpired_entry_is_served_with_its_age() {
747        let k = key(b"tenant-a");
748        let bytes = entry(&k);
749        let served = read_entry(
750            &bytes,
751            Expected {
752                kind: EntryKind::Request,
753                key: &k,
754                generations: GENS,
755                now: at(1_200),
756            },
757        )
758        .unwrap();
759        assert_eq!(served.age_ms, 200);
760        assert_eq!(served.created, at(1_000));
761        let EntryPayload::Request(payload) = served.payload else {
762            panic!("a request payload");
763        };
764        assert_eq!(payload.output, RawOutput::Label("billing".into()));
765        assert_eq!(payload.source_attempt_id, "att-1");
766        // At exactly `ttl_ms` the entry is still current.
767        assert_eq!(read(&bytes, &k, GENS, 1_500), Ok(500));
768        // The entry is the record canonical bytes of the document.
769        let doc = CacheEntry::parse(&bytes).unwrap();
770        assert_eq!(record_canonical_bytes(&doc).unwrap(), bytes);
771        assert!(doc.intact());
772    }
773
774    /// Spec 044, 3.4.3 and section 5: each check, in order, and only a
775    /// corrupt entry is deleted.
776    #[test]
777    fn every_failed_check_is_a_miss_with_its_diagnostic() {
778        let k = key(b"tenant-a");
779        let bytes = entry(&k);
780        let miss = |r: Result<u64, EntryMiss>| r.unwrap_err();
781
782        // Another schema, and bytes that are no document.
783        let other = String::from_utf8(bytes.clone())
784            .unwrap()
785            .replace(schema::CACHE_ENTRY, "rustev.cache-entry/9");
786        let m = miss(read(other.as_bytes(), &k, GENS, 1_200));
787        assert_eq!(m, EntryMiss::SchemaUnknown);
788        assert!(!m.deletes());
789        assert_eq!(m.diagnostic(), "entry_schema_unknown");
790        assert_eq!(
791            miss(read(br#"{"version":1}"#, &k, GENS, 1_200)),
792            EntryMiss::SchemaUnknown
793        );
794        assert_eq!(
795            miss(read(b"\x00garbage", &k, GENS, 1_200)),
796            EntryMiss::Corrupt
797        );
798
799        // A body altered without its digest is corrupt, and deleted.
800        let altered = String::from_utf8(bytes.clone())
801            .unwrap()
802            .replace("billing", "account");
803        let m = miss(read(altered.as_bytes(), &k, GENS, 1_200));
804        assert_eq!(m, EntryMiss::Corrupt);
805        assert!(m.deletes());
806        assert_eq!(m.diagnostic(), "entry_corrupt");
807
808        // Equal key ids over other material are a collision, as is
809        // another scope's key.
810        let mut forged = k.clone();
811        forged.material.push(0);
812        let m = miss(read(&bytes, &forged, GENS, 1_200));
813        assert_eq!(m, EntryMiss::Collision);
814        assert_eq!(m.diagnostic(), "entry_collision");
815        assert!(!m.deletes());
816        assert_eq!(
817            miss(read(&bytes, &key(b"tenant-b"), GENS, 1_200)),
818            EntryMiss::Collision
819        );
820        // The request's own material under another key id: the entry pairs
821        // them wrong, so it is corrupt.
822        let mut misnamed = k.clone();
823        misnamed.id = format!("sha256:{}", "0".repeat(64));
824        let m = miss(read(&bytes, &misnamed, GENS, 1_200));
825        assert_eq!(m, EntryMiss::Corrupt);
826        assert!(m.deletes());
827
828        // Either generation moved.
829        for gens in [
830            EntryGenerations {
831                namespace: 4,
832                ..GENS
833            },
834            EntryGenerations { scope: 8, ..GENS },
835        ] {
836            let m = miss(read(&bytes, &k, gens, 1_200));
837            assert_eq!(m, EntryMiss::GenerationAdvanced);
838            assert_eq!(m.diagnostic(), "generation_advanced");
839            assert!(!m.deletes());
840        }
841
842        // Expiry by evaluation time, and an entry from the future.
843        let m = miss(read(&bytes, &k, GENS, 1_501));
844        assert_eq!(m, EntryMiss::Expired);
845        assert_eq!(m.diagnostic(), "entry_expired");
846        assert!(!m.deletes());
847        let m = miss(read(&bytes, &k, GENS, 999));
848        assert_eq!(m, EntryMiss::FromFuture);
849        assert_eq!(m.diagnostic(), "entry_from_future");
850        assert!(!m.deletes());
851    }
852
853    /// Spec 044, 3.4.3 and 3.6.4: a decision entry carries its reused
854    /// requests, volatile entries and source digests, and reads back as
855    /// the memory tier holds it; a payload of another kind than the entry
856    /// names is corrupt, and an entry of the other kind is another key's.
857    #[test]
858    fn a_decision_entry_reads_back_and_checks_its_kind() {
859        let decision = key(b"tenant-a");
860        let reused = key(b"tenant-a");
861        let payload = DecisionPayload {
862            requests: vec![ReusedRequestPayload {
863                step: "topic".into(),
864                instance: vec![],
865                key_material: hex(&reused.material),
866                output: RawOutput::Label("billing".into()),
867                source_attempt_id: "att-1".into(),
868            }],
869            volatile: vec![],
870            judgment: ContentDigest::parse(&digest('e')).unwrap(),
871            run_record: ContentDigest::parse(&digest('f')).unwrap(),
872        };
873        let write = |payload: EntryPayload| {
874            write_entry(EntryWrite {
875                key: &decision,
876                policy: &policy(),
877                generations: GENS,
878                created: at(1_000),
879                ttl_ms: 500,
880                payload,
881                evidence: ContentDigest::parse(&digest('d')).unwrap(),
882            })
883            .unwrap()
884        };
885        let bytes = write(EntryPayload::Decision(payload.clone()));
886        assert!(String::from_utf8_lossy(&bytes).contains("\"kind\":\"decision\""));
887        let expected = |kind| Expected {
888            kind,
889            key: &decision,
890            generations: GENS,
891            now: at(1_100),
892        };
893        let served = read_entry(&bytes, expected(EntryKind::Decision)).unwrap();
894        let EntryPayload::Decision(read) = served.payload else {
895            panic!("a decision payload");
896        };
897        assert_eq!(read, payload);
898        let held = decision_entry(&decision, read, served.created).unwrap();
899        assert_eq!(held.created, at(1_000));
900        assert_eq!(held.source_run_record, payload.run_record);
901        let r = &held.requests[&("topic".to_string(), vec![])];
902        assert_eq!(r.key, reused);
903        assert_eq!(r.source_attempt_id, "att-1");
904        // A request key's read of a decision entry, and the reverse.
905        assert_eq!(
906            read_entry(&bytes, expected(EntryKind::Request)).unwrap_err(),
907            EntryMiss::Collision
908        );
909        assert_eq!(
910            read_entry(&entry(&decision), expected(EntryKind::Decision)).unwrap_err(),
911            EntryMiss::Collision
912        );
913        // A sealed entry whose kind is not its payload's was written wrong.
914        let mut doc = CacheEntry::parse(&bytes).unwrap();
915        doc.entry.kind = EntryKind::Request;
916        let mislabelled = CacheEntry::seal(doc.entry)
917            .unwrap()
918            .record_canonical()
919            .unwrap();
920        assert_eq!(
921            read_entry(&mislabelled, expected(EntryKind::Request)).unwrap_err(),
922            EntryMiss::Corrupt
923        );
924        // Key material that is not hexadecimal, or a request named twice.
925        let mut odd = payload.clone();
926        odd.requests[0].key_material.push('0');
927        assert!(decision_entry(&decision, odd, at(1_000)).is_none());
928        let mut upper = payload.clone();
929        upper.requests[0].key_material = upper.requests[0].key_material.to_uppercase();
930        assert!(decision_entry(&decision, upper, at(1_000)).is_none());
931        let mut twice = payload;
932        twice.requests.push(twice.requests[0].clone());
933        assert!(decision_entry(&decision, twice, at(1_000)).is_none());
934    }
935
936    #[test]
937    fn put_answers_name_their_diagnostics() {
938        assert_eq!(PutAnswer::Stored.diagnostic(), None);
939        assert_eq!(
940            PutAnswer::GenerationAdvanced.diagnostic(),
941            Some("generation_advanced")
942        );
943        assert_eq!(PutAnswer::OverBudget.diagnostic(), Some("over_budget"));
944        assert_eq!(PutAnswer::Conflict.diagnostic(), Some("store_conflict"));
945    }
946
947    #[test]
948    fn hex_is_lowercase_and_two_digits_per_byte() {
949        assert_eq!(hex(&[0x00, 0x0f, 0xa0, 0xff]), "000fa0ff");
950        assert_eq!(hex(&[]), "");
951        assert_eq!(unhex("000fa0ff"), Some(vec![0x00, 0x0f, 0xa0, 0xff]));
952        assert_eq!(unhex(""), Some(vec![]));
953        for bad in ["0", "0g", "0F", "+1"] {
954            assert_eq!(unhex(bad), None, "{bad}");
955        }
956    }
957}