pub struct QueryResultCache { /* private fields */ }Expand description
Thread-safe W-TinyLFU cache for query results.
Backed by moka::sync::Cache which provides lock-free reads via
Concurrent TinyLFU. Reverse DashMap indexes enable O(k) invalidation.
§Thread Safety
moka::sync::Cache is Send + Sync. All reverse indexes use DashMap
(fine-grained shard locking) and DashSet (also shard-locked). There is no
global mutex on the read path.
§Example
use fraiseql_core::cache::{QueryResultCache, CacheConfig};
use fraiseql_core::db::types::JsonbValue;
use serde_json::json;
let cache = QueryResultCache::new(CacheConfig::default());
// Cache a result
let result = vec![JsonbValue::new(json!({"id": 1, "name": "Alice"}))];
let fence = cache.invalidation_generation(); // snapshot before the read (#1079)
cache.put(
12345_u64,
result.clone(),
vec!["v_user".to_string()],
None, // use global TTL
None, // no entity type index
Some(fence), // discard the write if an invalidation raced the read
).unwrap();
// Retrieve from cache
if let Some(cached) = cache.get(12345).unwrap() {
println!("Cache hit! {} results", cached.len());
}Implementations§
Source§impl QueryResultCache
impl QueryResultCache
Sourcepub fn new(config: CacheConfig) -> Self
pub fn new(config: CacheConfig) -> Self
Sourcepub fn invalidation_generation(&self) -> u64
pub fn invalidation_generation(&self) -> u64
Snapshot of the invalidation generation, for fencing a cache write (#1079).
Take this before the database round trip and hand it to
put_arc. If any invalidation lands while the read is in flight,
the counter moves and the write is refused — the rows are still returned to the
caller, they are simply not cached.
Sourcepub const fn is_enabled(&self) -> bool
pub const fn is_enabled(&self) -> bool
Returns whether caching is enabled.
Used by CachedDatabaseAdapter to short-circuit key generation
and result clone overhead when caching is disabled.
Sourcepub const fn config(&self) -> &CacheConfig
pub const fn config(&self) -> &CacheConfig
The configuration this cache was built with (immutable after creation).
The admin surface reports the effective TTL and entry ceiling, which used to be hard-coded constants in the handler that happened to match a different cache’s defaults (#941).
Sourcepub fn get(&self, cache_key: u64) -> Result<Option<Arc<Vec<JsonbValue>>>>
pub fn get(&self, cache_key: u64) -> Result<Option<Arc<Vec<JsonbValue>>>>
Look up a cached result by its cache key.
Returns None when caching is disabled or the key is not present or expired.
Moka handles TTL expiry internally — if get() returns Some, the entry is live.
§Errors
This method is infallible. The Result return type is kept for API compatibility.
Sourcepub fn put_arc(
&self,
cache_key: u64,
result: Arc<Vec<JsonbValue>>,
accessed_views: Vec<String>,
ttl_override: Option<u64>,
entity_type: Option<&str>,
fence: Option<u64>,
) -> Result<()>
pub fn put_arc( &self, cache_key: u64, result: Arc<Vec<JsonbValue>>, accessed_views: Vec<String>, ttl_override: Option<u64>, entity_type: Option<&str>, fence: Option<u64>, ) -> Result<()>
Store query result in cache, accepting an already-Arc-wrapped result.
Preferred over put on the hot miss path: callers that already
hold an Arc<Vec<JsonbValue>> (e.g. CachedDatabaseAdapter) can store it
without an extra Vec clone.
§Arguments
cache_key- Cache key (fromgenerate_cache_key())result- Arc-wrapped query result to cacheaccessed_views- List of views accessed by this queryttl_override- Per-entry TTL in seconds;NoneusesCacheConfig::ttl_secondsentity_type- Optional GraphQL type name for entity-ID indexingfence- Generation snapshot frominvalidation_generation, taken before the database round trip whose rows are being stored.Nonestores unconditionally, and is only correct when the value did not come from a read that could have raced a mutation (a test fixture, or a synchronous re-population).
§Errors
This method is infallible. The Result return type is kept for API compatibility.
A fenced-out write is not an error — the caller already has its rows; they are
simply not cached. Turning a benign race into a request failure would be worse than
the staleness this prevents.
Sourcepub fn put(
&self,
cache_key: u64,
result: Vec<JsonbValue>,
accessed_views: Vec<String>,
ttl_override: Option<u64>,
entity_type: Option<&str>,
fence: Option<u64>,
) -> Result<()>
pub fn put( &self, cache_key: u64, result: Vec<JsonbValue>, accessed_views: Vec<String>, ttl_override: Option<u64>, entity_type: Option<&str>, fence: Option<u64>, ) -> Result<()>
Store query result in cache.
If caching is disabled, this is a no-op.
Wraps result in an Arc and delegates to put_arc.
Prefer put_arc when the caller already holds an Arc.
§Arguments
cache_key- Cache key (fromgenerate_cache_key())result- Query result to cacheaccessed_views- List of views accessed by this queryttl_override- Per-entry TTL in seconds;NoneusesCacheConfig::ttl_secondsentity_type- Optional GraphQL type name (e.g."User") for entity-ID indexing. When provided, each row’s"id"field is extracted and stored inentity_indexso thatinvalidate_by_entity()can perform selective eviction.
§Errors
This method is infallible. The Result return type is kept for API compatibility.
§Example
use fraiseql_core::cache::{QueryResultCache, CacheConfig};
use fraiseql_core::db::types::JsonbValue;
use serde_json::json;
let cache = QueryResultCache::new(CacheConfig::default());
let result = vec![JsonbValue::new(json!({"id": "uuid-1"}))];
let fence = cache.invalidation_generation();
cache.put(0xabc123, result, vec!["v_user".to_string()], None, Some("User"), Some(fence))?;Sourcepub fn invalidate_views(&self, views: &[ViewName]) -> Result<u64>
pub fn invalidate_views(&self, views: &[ViewName]) -> Result<u64>
Invalidate entries accessing specified views.
Uses the view_index for O(k) lookup instead of O(n) full-cache scan.
Keys accessing multiple views in views are deduplicated before invalidation.
§Arguments
views- List of view/table names modified by mutation
§Returns
Number of cache entries invalidated.
§Errors
This method is infallible. The Result return type is kept for API compatibility.
§Example
use fraiseql_core::cache::{QueryResultCache, CacheConfig};
use fraiseql_db::ViewName;
let cache = QueryResultCache::new(CacheConfig::default());
// After createUser mutation
let invalidated = cache.invalidate_views(&[ViewName::from("v_user")])?;
println!("Invalidated {} cache entries", invalidated);Sourcepub fn invalidate_by_entity(
&self,
entity_type: &str,
entity_id: &str,
) -> Result<u64>
pub fn invalidate_by_entity( &self, entity_type: &str, entity_id: &str, ) -> Result<u64>
Evict cache entries that contain a specific entity UUID.
Uses the entity_index for O(k) lookup. Entries not referencing this
entity are left untouched.
§Arguments
entity_type- GraphQL type name (e.g."User")entity_id- UUID string of the mutated entity
§Returns
Number of cache entries evicted.
§Errors
This method is infallible. The Result return type is kept for API compatibility.
Sourcepub fn metrics(&self) -> Result<CacheMetrics>
pub fn metrics(&self) -> Result<CacheMetrics>
Get cache metrics snapshot.
Returns a consistent snapshot of current counters. Individual fields may be updated independently (atomics), so the snapshot is not a single atomic transaction, but is accurate enough for monitoring.
§Errors
This method is infallible. The Result return type is kept for API compatibility.
§Example
use fraiseql_core::cache::{QueryResultCache, CacheConfig};
let cache = QueryResultCache::new(CacheConfig::default());
let metrics = cache.metrics()?;
println!("Hit rate: {:.1}%", metrics.hit_rate() * 100.0);
println!("Size: {} / {} entries", metrics.size, 10_000);Sourcepub fn clear(&self) -> Result<()>
pub fn clear(&self) -> Result<()>
Clear all cache entries.
Resets the store, reverse indexes, and memory_bytes synchronously.
The eviction listener will still fire asynchronously for each evicted entry,
but its index-cleanup operations will be no-ops on the already-cleared maps.
§Errors
This method is infallible. The Result return type is kept for API compatibility.
§Example
use fraiseql_core::cache::{QueryResultCache, CacheConfig};
let cache = QueryResultCache::new(CacheConfig::default());
cache.clear()?;Sourcepub fn run_pending_tasks(&self)
pub fn run_pending_tasks(&self)
Flush pending background tasks in the moka store.
Moka applies writes and evictions on a background schedule, so entry_count()
lags: a put followed immediately by metrics() reports zero entries. Tests
need this to synchronise before an assertion, and so does the admin stats
endpoint — an operator asking how many entries are cached wants the settled
number, not an estimate that reads as “nothing is cached” (#941).
Not on the query path: this walks the pending write buffer.