lance_core/cache/backend.rs
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Backend interface for cache implementors.
5//!
6//! This module defines the trait that custom cache backends must implement,
7//! along with the entry type they operate on. Most callers should
8//! use [`LanceCache`](super::LanceCache) instead of interacting with
9//! backends directly.
10//!
11//! # Migrating custom backends
12//!
13//! Cache keys are opaque 16-byte values. Store
14//! [`InternalCacheKey::as_bytes`] directly instead of decomposing a logical
15//! prefix, key string, and Rust type name. The physical namespace must also
16//! include [`CACHE_KEY_FORMAT`](super::CACHE_KEY_FORMAT), so a future key
17//! protocol produces cold misses instead of aliases. Persistent or tiered
18//! backends can route serializable values with [`CacheCodec::type_id`].
19//!
20//! Prefix invalidation and key inventory are intentionally not part of this
21//! interface: one-way digests cannot support either operation without
22//! retaining the logical strings that fixed-size keys are designed to remove.
23//! Existing callers should migrate removed symbols as follows:
24//! - replace `with_backend_and_prefix(backend, prefix)` with
25//! [`LanceCache::with_backend`](super::LanceCache::with_backend) followed by
26//! [`LanceCache::with_key_prefix`](super::LanceCache::with_key_prefix);
27//! - replace `invalidate_prefix` with [`LanceCache::clear`](super::LanceCache::clear)
28//! when clearing the shared backend is acceptable, or rotate a versioned
29//! namespace to leave older entries to age out;
30//! - remove uses of `prefix`, `keys`, and session key-inventory methods; opaque
31//! keys have no readable or enumerable equivalent.
32
33use std::any::Any;
34use std::pin::Pin;
35use std::sync::Arc;
36
37use async_trait::async_trait;
38use futures::Future;
39
40use crate::Result;
41use crate::deepsize::Context;
42
43use super::{CacheCodec, InternalCacheKey};
44
45/// A type-erased cache entry.
46pub type CacheEntry = Arc<dyn Any + Send + Sync>;
47
48/// The tier that would serve a read of an entry, as
49/// [`CacheBackend::peek_tier`] predicts it.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub enum CacheTier {
52 /// In RAM: a read is a memory hit.
53 Resident,
54 /// Not in RAM but held by a local tier below it, such as a persistent
55 /// store or a buffer of recently evicted entries: a read costs local I/O
56 /// at most, never the caller's loader.
57 Local,
58 /// Held by no tier: a read runs the caller's loader.
59 Absent,
60}
61
62/// Low-level pluggable cache backend.
63///
64/// Implementations store entries keyed by [`InternalCacheKey`] and return
65/// type-erased [`CacheEntry`] values.
66/// [`LanceCache`](super::LanceCache) handles key construction and type safety;
67/// backend authors only need to implement storage and eviction.
68#[async_trait]
69pub trait CacheBackend: Send + Sync + std::fmt::Debug {
70 /// Look up RAM only, without reading or promoting a persistent entry.
71 /// Backends without a resident lookup return a miss safely.
72 async fn get_resident(&self, _key: &InternalCacheKey) -> Option<CacheEntry> {
73 None
74 }
75
76 /// Report whether `key` is resident in RAM without counting as an access,
77 /// so a residency probe does not change what the backend evicts next.
78 /// The default uses [`get_resident`](Self::get_resident), which backends
79 /// with recency state may treat as an access.
80 async fn peek_resident(&self, key: &InternalCacheKey) -> bool {
81 self.get_resident(key).await.is_some()
82 }
83
84 /// Predict which tier would serve a read of `key`, without reading the
85 /// entry or counting as an access, so the check changes neither what the
86 /// backend evicts next nor its hit statistics. The default knows RAM only,
87 /// through [`peek_resident`](Self::peek_resident), and reports every other
88 /// entry as [`CacheTier::Absent`]; backends with a local tier below RAM
89 /// override it.
90 async fn peek_tier(&self, key: &InternalCacheKey) -> CacheTier {
91 if self.peek_resident(key).await {
92 CacheTier::Resident
93 } else {
94 CacheTier::Absent
95 }
96 }
97
98 /// Whether layered index planes should gate lower-plane RAM admission on
99 /// their sign plane being resident. Backends that admit plane entries
100 /// through their ordinary policy return `false`, so lower planes are
101 /// loaded and admitted like any other entry.
102 fn plane_admission_gated(&self) -> bool {
103 true
104 }
105
106 /// Read an entry without admitting a persistent hit into RAM.
107 /// Backends without this capability safely fall back to resident entries.
108 async fn get_without_promotion(
109 &self,
110 key: &InternalCacheKey,
111 _codec: Option<CacheCodec>,
112 ) -> Option<CacheEntry> {
113 self.get_resident(key).await
114 }
115
116 /// Gather selected rows from persistent storage, without RAM admission.
117 async fn get_rows(
118 &self,
119 _key: &InternalCacheKey,
120 _rows: &[u32],
121 _codec: Option<CacheCodec>,
122 ) -> Option<CacheEntry> {
123 None
124 }
125
126 /// Look up an entry by its key.
127 ///
128 /// `codec` is provided so that persistent backends can deserialize the
129 /// entry from storage. In-memory backends can ignore it. When `codec`
130 /// is `None`, the entry type does not support serialization yet and
131 /// must be stored in-memory.
132 ///
133 /// The goal is for all cache entry types to eventually have codecs,
134 /// at which point the `Option` will be removed.
135 async fn get(&self, key: &InternalCacheKey, codec: Option<CacheCodec>) -> Option<CacheEntry>;
136
137 /// Store an entry. `size_bytes` is used for eviction accounting.
138 ///
139 /// See [`get`](Self::get) for codec semantics.
140 async fn insert(
141 &self,
142 key: &InternalCacheKey,
143 entry: CacheEntry,
144 size_bytes: usize,
145 codec: Option<CacheCodec>,
146 );
147
148 /// Get an existing entry or compute it from `loader`.
149 ///
150 /// Implementations should deduplicate concurrent loads for the same key
151 /// so the loader runs at most once, unless caching is disabled. Disabled
152 /// caches may invoke each caller's loader independently.
153 ///
154 /// Returns `(entry, was_cached)` where `was_cached` is `true` if the entry
155 /// was already present in the cache (the loader was not invoked).
156 ///
157 /// See [`get`](Self::get) for codec semantics.
158 async fn get_or_insert<'a>(
159 &self,
160 key: &InternalCacheKey,
161 loader: Pin<Box<dyn Future<Output = Result<(CacheEntry, usize)>> + Send + 'a>>,
162 codec: Option<CacheCodec>,
163 ) -> Result<(CacheEntry, bool)>;
164
165 /// Remove all entries.
166 async fn clear(&self);
167
168 /// Number of entries currently stored (may flush pending operations).
169 async fn num_entries(&self) -> usize;
170
171 /// Total weighted size in bytes of all stored entries (may flush pending operations).
172 async fn size_bytes(&self) -> usize;
173
174 /// Approximate number of entries, callable from synchronous contexts.
175 /// Backends that cannot provide this cheaply should return 0.
176 fn approx_num_entries(&self) -> usize {
177 0
178 }
179
180 /// Approximate weighted size in bytes, callable from synchronous contexts.
181 /// Used as a `DeepSizeOf` fallback when exact entry traversal is unavailable.
182 /// Backends that cannot provide this cheaply should return 0.
183 ///
184 /// Assumes entries do not share underlying buffers; if they do, the
185 /// returned total may overcount.
186 fn approx_size_bytes(&self) -> usize {
187 0
188 }
189
190 /// Weighted capacity in bytes, or `None` when the backend has no fixed
191 /// capacity or cannot report it. Lets callers skip loading entries that
192 /// could never be retained.
193 fn capacity_bytes(&self) -> Option<usize> {
194 None
195 }
196
197 /// Computes the size of the entries currently held in memory.
198 ///
199 /// `size_of_entry` threads a shared [`Context`] through each value so
200 /// allocations shared by multiple entries are counted once. It returns
201 /// `None` when the value's concrete type was not registered by
202 /// [`LanceCache`](super::LanceCache); implementations should use the
203 /// entry's declared eviction size as a fallback in that case.
204 ///
205 /// Backends that can enumerate their in-memory entries should include the
206 /// physical key footprint in the returned total. The default returns
207 /// `None`, causing `LanceCache` to use [`approx_size_bytes`](Self::approx_size_bytes).
208 fn deep_size_of_entries(
209 &self,
210 _context: &mut Context,
211 _size_of_entry: &dyn Fn(&CacheEntry, &mut Context) -> Option<usize>,
212 ) -> Option<usize> {
213 None
214 }
215}