Skip to main content

CacheRuntime

Struct CacheRuntime 

Source
pub struct CacheRuntime { /* private fields */ }
Expand description

Ties policy resolution + content-addressed key derivation + single-flight compute-through into one call the dispatch path makes per tool. The enterprise injects a Redis backend + the real tenant; the OSS default is an in-process backend under a "local" tenant. This is the whole runtime contract for v2.40.0 — a hit returns before compute runs (so a budget gate placed after the lookup never sees it, the design decision).

Implementations§

Source§

impl CacheRuntime

Source

pub fn new(backend: Arc<dyn CacheBackend>, tenant: impl Into<String>) -> Self

Source

pub fn in_process() -> Self

In-process, single-tenant default (OSS runtime with no injected tier).

Source

pub fn process_local(tenant: impl Into<String>) -> Self

v2.89.0 — the OSS runtime a production flow run gets: the process-wide in-process tier, keyed to this run’s tenant.

§Why the split, and what each half prevents

This looks like a detail and is the difference between a cache and a decoration.

Build the whole CacheRuntime per flow run and the backend is empty every time: a tool called once per run — which is most tools — never hits anything, and v2.89.0 would ship a memoiser that memoises within a single run and forgets between them. Wired, tested, and worthless.

Share the whole CacheRuntime across runs and it is worse than worthless. tenant is a field of the runtime, not a parameter of the call, so one shared instance would key every tenant’s results under whichever tenant built it first — and the design decision puts the tenant IN the key precisely so that a mis-namespacing backend still cannot leak. A shared runtime with a fixed tenant defeats that from above the backend, where the key is derived.

So the BACKEND is process-wide (entries survive between runs, which is what makes it a cache) and the TENANT comes from the run (which is what keeps them apart). Constructing this is two Arc clones.

The enterprise v2.40.0 Redis tier replaces the backend here and inherits the same discipline unchanged — it is a different CacheBackend, not a different call site.

Source

pub fn dispatch<F, E>( &self, ir: &IRProgram, tool: &IRToolSpec, args: &[(String, String)], compute: F, ) -> Result<CacheOutcome, E>
where F: FnOnce() -> Result<Vec<u8>, E>,

Look up (or compute-and-store) a tool result. args is the full bound (name, value) set; the key: subset (if any) is applied here. compute runs ONLY on a miss and its error is never cached.

Source

pub fn dispatch_resolved<F, E>( &self, policy: Option<&ResolvedCachePolicy>, subject: &str, args: &[(String, String)], compute: F, ) -> Result<CacheOutcome, E>
where F: FnOnce() -> Result<Vec<u8>, E>,

v2.89.0 — the memoisation body, against an already-resolved policy.

subject is what the entry is keyed to — a tool’s name, or a store’s name for a retrieve. policy: None means “nothing memoises this”: compute runs and the value comes back as CacheOutcome::Uncached, which the caller uses exactly as a Miss minus the audit signal.

§The ordering that the design decision rests on

A hit returns BEFORE compute is called. That is not an optimisation, it is the guarantee: the caller places its budget charge inside compute’s caller, so a hit cannot decrement a budget { rate: … } quota. v2.40.0’s plan calls this “structurally guaranteed by ordering the cache lookup before the budget gate” — the structure is right here.

Source

pub fn probe( &self, policy: Option<&ResolvedCachePolicy>, subject: &str, args: &[(String, String)], ) -> CacheProbe

v2.89.0 — look, without computing.

§Why the seam had to split

dispatch_resolved takes a compute closure, which is the right shape when the work is synchronous and owns nothing. The real tool-call path is neither: the work between the lookup and the value is async, it borrows &mut DispatchCtx, and it is not one call but four in sequence — the budget charge, the lease charge, the concurrency permit, then the vendor dispatch. None of that fits inside an FnOnce() -> Result<Vec<u8>, E>, and contorting it to fit would have meant either blocking the executor or duplicating the memoisation law at the call site.

So the law splits into the two moments an async caller actually has: probe before the work, store after it. dispatch_resolved is now implemented in terms of these, so the synchronous seam v2.40.0 designed and the asynchronous one v2.89.0 needed run the SAME key derivation, the same TTL parse and the same namespace — one law, two ways in.

The ordering the design decision rests on is the caller’s to keep: a CacheProbe::Hit means the call must not be dispatched AND no budget charged. Returning early on a hit is what makes “a hit never consumes a quota” structural rather than hopeful, and it is asserted from a real deploy in cache_hits.rs.

Source

pub fn store(&self, slot: &CacheSlot, value: Vec<u8>)

v2.89.0 — fill the slot a CacheProbe::Miss reserved.

Errors are never stored — that is the caller’s decision, and it is expressed by simply not calling this. Oversized values are dropped by the backend, never truncated into a wrong value.

Source

pub fn invalidate(&self, cache_name: &str)

Flush a cache namespace (called when an emit fires on one of its invalidate_on: channels).

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more