#[cachekit]Expand description
Cache the result of an async function under an interop/v1 cache key.
Keys are {namespace}:{operation}:{args_hash} per the protocol’s
interop-mode spec, so the same entry is addressable from the Python and
TypeScript SDKs — Python’s @cache(interop="get_user", namespace="users") computes the identical key for the same arguments.
§Attributes
client = <ident>(required): Name of the&CacheKitparameter.ttl = <integer>(required): TTL in seconds.interop = "<string>"(required): explicit, language-neutral operation segment (^[a-z0-9][a-z0-9._-]{0,63}$) — same meaning as Python’sinterop=/ TypeScript’sinterop:.namespace = "<string>"(required): interop/v1 namespace segment, same grammar.nsandnsapiare reserved as namespaces (a compile error — the CachekitIO server parses those key prefixes); operations are unaffected. Neither segment may contain..(a compile error — the server rejects..anywhere in a key); a lone.is fine.secure(optional flag): require encryption — every call fails withCachekitError::Configon a client without it — and fail closed on every backend error (see Reliability behaviour). The flag is not what encrypts: on a client with encryption configured, a function without it stores and reads ciphertext too, because the client’s value methods encrypt.
§Requirements
- The function must return
Result<T, E>whereE: From<CachekitError>—CachekitErroritself, or the application’s own error type. The body’s errors reach the caller unchanged and are never cached; cache-layer errors that propagate (see Reliability behaviour) arrive converted throughFrom. The error type is taken from the return type as written, so a one-argument alias such asanyhow::Result<T>works. - Every non-client argument must be a plain identifier (no destructuring
patterns, no
self) and must convert intocachekit::interop::InteropValueviaFrom(bool,i32/i64/u32/u64/i128,f64,&str/String,Uuid). Unsupported argument types fail at compile time. - In-model values outside the interop ranges fail at CALL time with
CachekitError::Serializationbefore the body runs: non-finite floats (NAN,±INFINITY) andi128outside[-2^63, 2^64-1]. This fail-loud contract matches the Python and TypeScript SDKs — silently running uncached would mask cross-SDK key divergence. - The client must be built without
.namespace()— interop keys carry their own namespace segment, and reads fail closed on a namespaced client. - A stored entry that cannot be decoded as the return type
(
CachekitError::Serialization) is treated as a miss and overwritten (self-healing). On a client with encryption configured,secureor not, this covers only post-decrypt decode failures: an entry that fails AES-GCM authentication raisesCachekitError::Encryption, which propagates (fail-closed). The failed read drops the key’s L1 copy and leaves the backend entry in place, so the next call reads the backend again: it fails while that entry remains, succeeds once any process replaces it with ciphertext this client can decrypt, and is a miss once it expires or is deleted.
§Requirements (continued)
- The function must be
async— the generated body awaits the cache read, the cold-miss single-flight, and SWR refresh scheduling. A sync function is a clear compile-time error, never a silent no-op.
§Reliability behaviour
- Stale-while-revalidate (client SWR enabled — the default with the
l1feature on native targets): an L1 hit past the client’s freshness threshold (swr_threshold_ratio× entry TTL, ±10% jitter) but before hard expiry is returned immediately — the caller never blocks on a merely-stale entry — while one background task re-executes the function. Its version-checked commit rewrites both layers only if no newer set or delete replaced the stale entry; a successful commit renews the L1 hard expiry with the full write-path TTL. Refresh dedup takes theCacheKit::single_flightlocks without waiting on them: N concurrent stale readers trigger at most one re-execution per process (and, on lock-capable backends, per fleet), and none while the lock call fails, in which case the stale value serves until hard expiry. Hard-expired entries always take the normal blocking miss path. The refresh task needs a tokio runtime (skipped otherwise — the stale value was already served) and captures arguments by owned copy (Clone/ToOwned— already required for key derivation); the future must beSend, soTand anything the body holds across an.awaitmust be too.Eneed not be: the refresh drops the body’s error before it awaits again, soBox<dyn Error>works. Refresh failures are absorbed: the stale value keeps serving until hard expiry, where the blocking path surfaces errors. - Graceful degradation: on an outage-class backend failure —
transient, timeout, an open circuit breaker, or a backpressure shed —
the plain path fails open: the function executes uncached and its
result is returned.
Permanent and authentication errors propagate even on the plain path
(a wrong API key must fail loudly, not silently disable caching
forever). With
secure, every backend and decryption error fails closed and propagates: an encrypted workload never silently degrades. Without the flag, a client with encryption configured still propagates decryption errors and still encrypts what it writes; only the outage-class backend errors above fail open. - Cold-miss single-flight: concurrent calls that miss on the same
key are collapsed to one execution per process (and per fleet, when
the backend supports distributed fill locks — CachekitIO and Redis do,
with the
reliabilityfeature). Waiters re-read the filled entry instead of recomputing. Seecachekit::flight.
§Example
ⓘ
#[cachekit(client = cache, ttl = 60, interop = "get_user", namespace = "users")]
async fn get_user(cache: &CacheKit, id: u64) -> Result<User, CachekitError> {
Ok(User { name: format!("User {id}") })
}