Skip to main content

rustev_contract/
optimization.rs

1//! Neutral runtime optimization policy and evidence vocabulary (spec 017).
2//!
3//! These types describe behavior that is part of a plan and observations that
4//! belong to a run. They do not perform I/O or grant authority. Spec 017
5//! covers runtime-planned batching and bounded memory caching; spec 044 adds
6//! shared calls, the persistent tier and the decision-result cache.
7
8use serde::{Deserialize, Serialize};
9
10/// Most diagnostics one request's optimization record keeps. Later ones are
11/// dropped and the last kept entry says so.
12pub const MAX_OPTIMIZATION_DIAGNOSTICS: usize = 8;
13
14/// Plan-identified controls for reusable semantic work.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
16#[serde(deny_unknown_fields)]
17pub struct OptimizationPolicy {
18    pub key_schema_version: u32,
19    pub cache_namespace_version: u64,
20    pub admission: OptimizationAdmission,
21    pub expiry: ExpiryBasis,
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub batch: Option<BatchPolicy>,
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub memory_cache: Option<MemoryCachePolicy>,
26    /// Concurrent duplicate suppression (spec 044, 3.2).
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub shared_calls: Option<SharedCallPolicy>,
29    /// The persistent request-level tier behind the host's store (spec 044,
30    /// 3.4).
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub persistent_cache: Option<PersistentCachePolicy>,
33    /// The decision-result cache (spec 044, 3.6). Only a `rustev.plan/3`
34    /// carries it (3.1).
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub decision_cache: Option<DecisionCachePolicy>,
37}
38
39/// Bounds of shared calls (spec 044, 3.1 and 3.2).
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
41#[serde(deny_unknown_fields)]
42pub struct SharedCallPolicy {
43    /// Shared calls in flight on one runtime.
44    pub max_shared_calls: u32,
45    /// Waiters on one shared call, its leader included.
46    pub max_waiters_per_call: u32,
47    pub allocation: SharedChargeAllocation,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
51#[serde(rename_all = "snake_case")]
52pub enum SharedChargeAllocation {
53    /// The waiter that owns the attempt when it ends settles the whole
54    /// observed charge; every other waiter's share is 0 (spec 044, 3.2.4).
55    FinalOwner,
56}
57
58/// The persistent tier's bounds and the host store's declared terms (spec
59/// 044, 3.1 and 3.4).
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
61#[serde(deny_unknown_fields)]
62pub struct PersistentCachePolicy {
63    pub store_contract: StoreContract,
64    pub max_entry_bytes: u64,
65    pub max_scope_bytes: u64,
66    pub max_namespace_bytes: u64,
67    pub ttl_ms: u64,
68    pub on_store_failure: StoreFailurePolicy,
69}
70
71/// The host store's terms, each a declared string recorded as given. Rustev
72/// checks only that each is stated and that the store reports the same
73/// contract; it owns none of them (spec 044, 2.1).
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
75#[serde(deny_unknown_fields)]
76pub struct StoreContract {
77    pub schema: String,
78    pub namespace: String,
79    pub durability: String,
80    pub encryption: String,
81    pub atomicity: String,
82    pub conflict: String,
83    pub expiry: String,
84    pub invalidation: String,
85    pub erasure: String,
86}
87
88impl StoreContract {
89    /// Every term in declaration order, with its name.
90    pub fn terms(&self) -> [(&'static str, &str); 9] {
91        [
92            ("schema", &self.schema),
93            ("namespace", &self.namespace),
94            ("durability", &self.durability),
95            ("encryption", &self.encryption),
96            ("atomicity", &self.atomicity),
97            ("conflict", &self.conflict),
98            ("expiry", &self.expiry),
99            ("invalidation", &self.invalidation),
100            ("erasure", &self.erasure),
101        ]
102    }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
106#[serde(rename_all = "snake_case")]
107pub enum StoreFailurePolicy {
108    /// A read failure refuses under the policy's `admission`; a write
109    /// failure is recorded and the output kept (spec 044, 3.4.2).
110    FailClosed,
111    /// A failure is a miss and a diagnostic.
112    ContinueWithoutCache,
113}
114
115/// The decision-result cache's tiers, bounds and declared sources (spec
116/// 044, 3.1 and 3.6).
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
118#[serde(deny_unknown_fields)]
119pub struct DecisionCachePolicy {
120    /// In lookup order, memory before persistent (3.3). The persistent tier
121    /// is the store `persistent_cache` declares.
122    pub tiers: Vec<DecisionCacheTier>,
123    pub max_entry_bytes: u64,
124    /// Entries of the memory tier; present exactly when it is a tier.
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub max_entries: Option<u64>,
127    pub ttl_ms: u64,
128    /// Input names, and `step:<id>` for an acquisition step, refreshed on
129    /// every hit (3.6.2).
130    pub volatile: Vec<String>,
131    /// Upstream-origin dimensions the host supplies as scope handles (3.5).
132    pub key_dimensions: Vec<String>,
133    /// Omitted means `unresolved` (3.6.5); always written.
134    #[serde(default)]
135    pub on_refresh_failure: RefreshFailurePolicy,
136}
137
138#[derive(
139    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
140)]
141#[serde(rename_all = "snake_case")]
142pub enum DecisionCacheTier {
143    Memory,
144    Persistent,
145}
146
147/// What a failed refresh supplies (spec 044, 3.6.5). No policy supplies a
148/// value older than its declared freshness or turns a failure into a value.
149#[derive(
150    Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
151)]
152#[serde(rename_all = "snake_case")]
153pub enum RefreshFailurePolicy {
154    /// The failure is supplied as it is.
155    #[default]
156    Unresolved,
157    /// The hit is abandoned and the decision runs again with no reuse.
158    Recompute,
159    /// A volatile input's stored snapshot entry is supplied with its
160    /// original as-of time, and the core's freshness check decides.
161    ServeCachedWithinFreshness,
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
165#[serde(rename_all = "snake_case")]
166pub enum OptimizationAdmission {
167    /// Refuse when an enabled optimization cannot safely admit the request.
168    Refuse,
169    /// Execute independently without reuse and record the reason.
170    ExecuteIndependent,
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
174#[serde(rename_all = "snake_case")]
175pub enum ExpiryBasis {
176    /// The monotonic clock injected into the runtime. It is process-relative,
177    /// which is sound for a memory cache that dies with the process.
178    InjectedRuntimeTime,
179}
180
181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
182#[serde(deny_unknown_fields)]
183pub struct BatchPolicy {
184    pub max_members: u32,
185    pub max_canonical_bytes: u64,
186    pub max_members_per_scope: u32,
187    /// Upper bound on time a member may wait for peers. Batches are formed
188    /// within one decision's admission wave, so the runtime waits zero.
189    pub max_queue_delay_ms: u64,
190    pub max_backend_work: u64,
191    pub allocation: BatchChargeAllocation,
192}
193
194/// Deterministic settlement of one observed batch charge across its logical
195/// members. Member order is the runtime-recorded batch index order.
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
197#[serde(rename_all = "snake_case")]
198pub enum BatchChargeAllocation {
199    /// Divide integer units evenly. Members with the lowest indices receive
200    /// one extra unit until the remainder is exhausted. The shares are
201    /// allocation evidence; the batch total is settled once, by the charge
202    /// owner, against the summed member reservations.
203    EvenRemainderByMemberIndex,
204}
205
206/// Bounds apply to one plan's cache namespace; no plan's policy evicts
207/// another plan's entries.
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
209#[serde(deny_unknown_fields)]
210pub struct MemoryCachePolicy {
211    /// Opt in to eligible acquisition pages (spec 036, 3.13.7).
212    /// Absence disables reuse; true is the only explicit wire value.
213    #[serde(
214        default,
215        skip_serializing_if = "std::ops::Not::not",
216        deserialize_with = "acquisition_pages"
217    )]
218    // Despite its name, constructed_schema emits only a literal-true boolean
219    // schema, matching this opt-in field's wire contract too.
220    #[schemars(schema_with = "crate::acquisition_record::constructed_schema")]
221    pub acquisition_pages: bool,
222    pub max_bytes: u64,
223    pub max_entries: u64,
224    pub max_entry_bytes: u64,
225    pub max_scope_bytes: u64,
226    pub ttl_ms: u64,
227    pub eviction: EvictionOrder,
228}
229
230#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
231#[serde(rename_all = "snake_case")]
232pub enum EvictionOrder {
233    /// First admitted, first evicted. Cache hits do not reorder entries. A
234    /// per-scope bound evicts only that scope's oldest entries.
235    Fifo,
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
239#[serde(rename_all = "snake_case")]
240pub enum OptimizationMechanism {
241    Independent,
242    Batch,
243    MemoryCache,
244    /// The output of an attempt shared with concurrent equivalent requests
245    /// (spec 044, 3.2.5).
246    SharedCall,
247    /// The persistent request-level tier behind the host's store (spec
248    /// 044, 3.3 and 3.4).
249    PersistentCache,
250    /// A request supplied from a decision-result cache entry, or refused
251    /// with its decision (spec 044, 3.6.3 and 3.6.6).
252    DecisionCache,
253}
254
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
256#[serde(rename_all = "snake_case")]
257pub enum OptimizationOutcome {
258    Bypassed,
259    Miss,
260    Hit,
261    Batched,
262    Refused,
263}
264
265/// Per-logical-request runtime observations. Secret key material and cached
266/// content are deliberately absent.
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
268#[serde(deny_unknown_fields)]
269pub struct OptimizationRecord {
270    pub mechanism: OptimizationMechanism,
271    pub key_schema_version: u32,
272    pub outcome: OptimizationOutcome,
273    #[serde(default, skip_serializing_if = "Option::is_none")]
274    pub entry_id: Option<String>,
275    #[serde(default, skip_serializing_if = "Option::is_none")]
276    pub batch_id: Option<String>,
277    #[serde(default, skip_serializing_if = "Option::is_none")]
278    pub batch_member: Option<u32>,
279    /// The attempt that produced the supplied output. For a cache hit it is
280    /// an attempt of an earlier decision, and the record has no attempt of
281    /// its own; replay takes it as the output's origin.
282    #[serde(default, skip_serializing_if = "Option::is_none")]
283    pub source_attempt_id: Option<String>,
284    pub age_ms: u64,
285    pub invalidation_generation: u64,
286    pub charge_owner: bool,
287    pub allocated_charge_units: u64,
288    pub shared_liability_units: u64,
289    /// Every refusal or diagnostic, in the order observed, at most
290    /// [`MAX_OPTIMIZATION_DIAGNOSTICS`].
291    #[serde(default, skip_serializing_if = "Vec::is_empty")]
292    pub diagnostics: Vec<String>,
293    /// This request's part in a shared call (spec 044, 3.2.5).
294    #[serde(default, skip_serializing_if = "Option::is_none")]
295    pub shared: Option<SharedRecord>,
296}
297
298/// One waiter's view of a shared call. Waiter ids are opaque and
299/// runtime-generated, never decision ids, so a tenant-only scope exposes no
300/// principal's decisions to another (spec 044, decision 5).
301#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
302#[serde(deny_unknown_fields)]
303pub struct SharedRecord {
304    pub shared_call_id: String,
305    pub attempt_id: String,
306    pub waiter_id: String,
307    pub role: SharedRole,
308    #[serde(default, skip_serializing_if = "Option::is_none")]
309    pub left: Option<SharedLeave>,
310    #[serde(default, skip_serializing_if = "Option::is_none")]
311    pub handed_off_to: Option<String>,
312    #[serde(default, skip_serializing_if = "Option::is_none")]
313    pub received_from: Option<String>,
314    pub final_owner: bool,
315}
316
317#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
318#[serde(rename_all = "snake_case")]
319pub enum SharedRole {
320    Led,
321    Joined,
322}
323
324/// Why a waiter left before the shared attempt ended.
325#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
326#[serde(rename_all = "snake_case")]
327pub enum SharedLeave {
328    Cancelled,
329    Deadline,
330}
331
332/// The decision-result cache's part in one decision, a `rustev.run/3`
333/// member (spec 044, 3.6.6). Request indices are positions in the run
334/// record's `requests`.
335#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
336#[serde(deny_unknown_fields)]
337pub struct DecisionCacheRecord {
338    pub outcome: DecisionCacheOutcome,
339    /// The entry served from; absent unless `hit` or `refreshed`.
340    #[serde(default, skip_serializing_if = "Option::is_none")]
341    pub entry_id: Option<String>,
342    /// The digest of the run record of the decision that wrote the entry.
343    #[serde(default, skip_serializing_if = "Option::is_none")]
344    pub source_run_record: Option<crate::ids::ContentDigest>,
345    /// By evaluation time, as for a persistent entry.
346    #[serde(default, skip_serializing_if = "Option::is_none")]
347    pub age_ms: Option<u64>,
348    /// The store's generations, for an entry of the persistent tier.
349    #[serde(default, skip_serializing_if = "Option::is_none")]
350    pub generations: Option<crate::cache_entry::EntryGenerations>,
351    /// Requests supplied from the entry.
352    #[serde(default, skip_serializing_if = "Vec::is_empty")]
353    pub reused: Vec<u32>,
354    /// Requests inside the refresh closure, dispatched again.
355    #[serde(default, skip_serializing_if = "Vec::is_empty")]
356    pub refreshed: Vec<u32>,
357    /// Requests outside the closure dispatched for `closure_mismatch`.
358    #[serde(default, skip_serializing_if = "Vec::is_empty")]
359    pub mismatched: Vec<u32>,
360    /// Volatile inputs supplied from the entry under
361    /// `serve_cached_within_freshness`.
362    #[serde(default, skip_serializing_if = "Vec::is_empty")]
363    pub substituted: Vec<SubstitutedInput>,
364    /// The policy applied, present when a refresh failed.
365    #[serde(default, skip_serializing_if = "Option::is_none")]
366    pub on_refresh_failure: Option<RefreshFailurePolicy>,
367    /// The served run abandoned under `recompute` (3.6.5); never supplied
368    /// to the core or to replay.
369    #[serde(default, skip_serializing_if = "Option::is_none")]
370    pub abandoned: Option<AbandonedRun>,
371    /// At most [`MAX_OPTIMIZATION_DIAGNOSTICS`], as a request's record.
372    #[serde(default, skip_serializing_if = "Vec::is_empty")]
373    pub diagnostics: Vec<String>,
374}
375
376#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
377#[serde(rename_all = "snake_case")]
378pub enum DecisionCacheOutcome {
379    /// Served from an entry with no request dispatched again.
380    Hit,
381    /// Served from an entry with at least one request dispatched again.
382    Refreshed,
383    /// No usable entry, or a hit abandoned under `recompute`.
384    Miss,
385    /// Neither served nor admitted; run under `execute_independent`.
386    Bypassed,
387    /// The same conditions under `admission: refuse`.
388    Refused,
389}
390
391/// A served run abandoned under `on_refresh_failure: recompute` (spec 044,
392/// 3.6.6): the records its dispatched requests and its acquisitions would
393/// have had, with their attempts and charges.
394#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
395#[serde(deny_unknown_fields)]
396pub struct AbandonedRun {
397    #[serde(default, skip_serializing_if = "Vec::is_empty")]
398    pub requests: Vec<crate::run::RequestRecord>,
399    #[serde(default, skip_serializing_if = "Vec::is_empty")]
400    pub acquisitions: Vec<crate::acquisition_record::AcquisitionRecord>,
401}
402
403/// A volatile input's stored snapshot entry supplied after a failed
404/// refresh (spec 044, 3.6.5).
405#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
406#[serde(deny_unknown_fields)]
407pub struct SubstitutedInput {
408    pub input: String,
409    pub entry_digest: crate::ids::ContentDigest,
410}
411
412impl OptimizationRecord {
413    /// Append a diagnostic without exceeding the bound.
414    pub fn diagnose(&mut self, diagnostic: impl Into<String>) {
415        match self.diagnostics.len() {
416            n if n + 1 < MAX_OPTIMIZATION_DIAGNOSTICS => self.diagnostics.push(diagnostic.into()),
417            n if n + 1 == MAX_OPTIMIZATION_DIAGNOSTICS => {
418                self.diagnostics.push("further diagnostics dropped".into());
419            }
420            _ => {}
421        }
422    }
423}
424
425fn acquisition_pages<'de, D: serde::Deserializer<'de>>(d: D) -> Result<bool, D::Error> {
426    if !bool::deserialize(d)? {
427        return Err(serde::de::Error::custom(
428            "omit acquisition_pages when disabled",
429        ));
430    }
431    Ok(true)
432}