Skip to main content

lance/
session.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::collections::HashMap;
5use std::sync::Arc;
6
7use lance_core::cache::{CacheBackend, LanceCache, QuickCacheBackend};
8use lance_core::deepsize::DeepSizeOf;
9use lance_core::{Error, Result};
10use lance_index::IndexType;
11use lance_io::object_store::ObjectStoreRegistry;
12use lance_io::spill::{LocalSpillStore, SpillStore};
13
14use crate::dataset::{DEFAULT_INDEX_CACHE_SIZE, DEFAULT_METADATA_CACHE_SIZE};
15use crate::session::caches::GlobalMetadataCache;
16use crate::session::index_caches::GlobalIndexCache;
17
18use self::index_extension::IndexExtension;
19
20pub(crate) mod caches;
21pub mod index_caches;
22pub(crate) mod index_extension;
23
24/// A user session holds the runtime state for a [`crate::Dataset`]
25///
26/// A session will be created automatically when a Dataset is opened.  However, you
27/// can manually create the session and provide it to the Dataset builder in order
28/// to share runtime state between multiple datasets.
29///
30/// This can be used to share caches between multiple datasets, increasing the hit
31/// rate and reducing the amount of memory used.
32///
33/// A session contains two different caches:
34///  - The index cache is used to cache opened indices and will cache index data
35///  - The metadata cache is used to cache a variety of dataset metadata (more
36///    details can be found in the [performance guide](https://lance.org/guide/performance/)
37#[derive(Clone)]
38pub struct Session {
39    /// Global cache for opened indices.
40    ///
41    /// Sub-caches are created from this cache for each dataset by adding the
42    /// URI and index UUID as a key prefix. If there is a fragment re-use index,
43    /// that is also in the key prefix. This prevents collisions between different
44    /// datasets and indices.
45    pub(crate) index_cache: GlobalIndexCache,
46
47    /// Global cache for file metadata.
48    ///
49    /// Sub-caches are created from this cache for each dataset by adding the
50    /// URI as a key prefix. See the [`LanceDataset::metadata_cache`] field.
51    /// This prevents collisions between different datasets.
52    pub(crate) metadata_cache: caches::GlobalMetadataCache,
53
54    pub(crate) index_extensions: HashMap<(IndexType, String), Arc<dyn IndexExtension>>,
55
56    store_registry: Arc<ObjectStoreRegistry>,
57
58    spill_store: Arc<dyn SpillStore>,
59}
60
61impl DeepSizeOf for Session {
62    fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
63        let mut size = 0;
64        // Measure the actual cache contents through the wrapper types
65        size += self.index_cache.deep_size_of_children(context);
66        size += self.metadata_cache.deep_size_of_children(context);
67        for ext in self.index_extensions.values() {
68            size += ext.deep_size_of_children(context);
69        }
70        size
71    }
72}
73
74impl std::fmt::Debug for Session {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        f.debug_struct("Session")
77            .field(
78                "index_cache",
79                &format!("IndexCache(items={})", self.index_cache.0.approx_size(),),
80            )
81            .field(
82                "file_metadata_cache",
83                &format!("LanceCache(items={})", self.metadata_cache.0.approx_size(),),
84            )
85            .field(
86                "index_extensions",
87                &self.index_extensions.keys().collect::<Vec<_>>(),
88            )
89            .finish()
90    }
91}
92
93impl Session {
94    /// Create a new session.
95    ///
96    /// Parameters:
97    ///
98    /// - ***index_cache_size***: the size of the index cache, backed by
99    ///   [`QuickCacheBackend`].
100    /// - ***metadata_cache_size***: the size of the metadata cache, backed by
101    ///   [`QuickCacheBackend`].
102    /// - ***store_registry***: the object store registry to use when opening
103    ///   datasets. This determines which schemes are available, and also allows
104    ///   re-using object stores.
105    pub fn new(
106        index_cache_size: usize,
107        metadata_cache_size: usize,
108        store_registry: Arc<ObjectStoreRegistry>,
109    ) -> Self {
110        Self {
111            index_cache: GlobalIndexCache(LanceCache::with_backend(Arc::new(
112                QuickCacheBackend::with_capacity(index_cache_size),
113            ))),
114            metadata_cache: GlobalMetadataCache(LanceCache::with_backend(Arc::new(
115                QuickCacheBackend::with_capacity(metadata_cache_size),
116            ))),
117            index_extensions: HashMap::new(),
118            store_registry,
119            spill_store: Arc::new(LocalSpillStore::default()),
120        }
121    }
122
123    /// Create a session with a custom index cache backend.
124    ///
125    /// The provided backend will be used for caching index data. The metadata
126    /// cache uses a [`QuickCacheBackend`] with the given capacity.
127    pub fn with_index_cache_backend(
128        index_cache_backend: Arc<dyn CacheBackend>,
129        metadata_cache_size: usize,
130        store_registry: Arc<ObjectStoreRegistry>,
131    ) -> Self {
132        Self {
133            index_cache: GlobalIndexCache(LanceCache::with_backend(index_cache_backend)),
134            metadata_cache: GlobalMetadataCache(LanceCache::with_backend(Arc::new(
135                QuickCacheBackend::with_capacity(metadata_cache_size),
136            ))),
137            index_extensions: HashMap::new(),
138            store_registry,
139            spill_store: Arc::new(LocalSpillStore::default()),
140        }
141    }
142
143    /// Replace the spill store used by this session.
144    ///
145    /// This is a builder-style method that consumes and returns `self`, making
146    /// it easy to chain during session construction:
147    ///
148    /// ```rust,no_run
149    /// # use lance::session::Session;
150    /// # use lance_io::spill::LocalSpillStore;
151    /// # use std::sync::Arc;
152    /// let session = Session::default()
153    ///     .with_spill_store(Arc::new(LocalSpillStore::with_cap(1 << 30).unwrap()));
154    /// ```
155    pub fn with_spill_store(mut self, store: Arc<dyn SpillStore>) -> Self {
156        self.spill_store = store;
157        self
158    }
159
160    /// Return a reference to the session's spill store.
161    ///
162    /// Callers use this to obtain reclaimable scratch space for intermediate
163    /// state that overflows memory (e.g. index builders).
164    pub fn spill_store(&self) -> &dyn SpillStore {
165        &*self.spill_store
166    }
167
168    /// Register a new index extension.
169    ///
170    /// A name can only be registered once per type of index extension.
171    ///
172    /// Parameters:
173    ///
174    /// - ***name***: the name of the extension.
175    /// - ***extension***: the extension to register.
176    pub fn register_index_extension(
177        &mut self,
178        name: String,
179        extension: Arc<dyn IndexExtension>,
180    ) -> Result<()> {
181        match extension.index_type() {
182            IndexType::Vector => {
183                if self
184                    .index_extensions
185                    .contains_key(&(IndexType::Vector, name.clone()))
186                {
187                    return Err(Error::invalid_input(format!(
188                        "{name} is already registered"
189                    )));
190                }
191
192                if let Some(ext) = extension.to_vector() {
193                    self.index_extensions
194                        .insert((IndexType::Vector, name), ext.to_generic());
195                } else {
196                    return Err(Error::invalid_input(format!(
197                        "{name} is not a vector index extension"
198                    )));
199                }
200            }
201            _ => {
202                return Err(Error::invalid_input(format!(
203                    "scalar index extension is not support yet: {}",
204                    extension.index_type()
205                )));
206            }
207        }
208
209        Ok(())
210    }
211
212    /// Return the current size of the session in bytes
213    ///
214    /// Keep in mind that this is not trivial to compute, as we will need to walk the caches
215    pub fn size_bytes(&self) -> u64 {
216        // We re-expose deep_size_of here so that users don't
217        // need the deepsize crate themselves (e.g. to use deep_size_of)
218        self.deep_size_of() as u64
219    }
220
221    /// Get the approximate number of items in the session.
222    ///
223    /// This is a rough estimate of the number of items in the session.  It is not
224    /// exact and is not guaranteed to be accurate.
225    pub fn approx_num_items(&self) -> usize {
226        self.index_cache.0.approx_size()
227            + self.metadata_cache.0.approx_size()
228            + self.index_extensions.len()
229    }
230
231    /// Get the object store registry.
232    pub fn store_registry(&self) -> Arc<ObjectStoreRegistry> {
233        self.store_registry.clone()
234    }
235
236    /// Get a reference to the raw metadata cache (for use in index reconstruction).
237    pub fn file_metadata_cache(&self) -> &LanceCache {
238        &self.metadata_cache.0
239    }
240
241    /// Fetch statistics for the metadata cache
242    pub async fn metadata_cache_stats(&self) -> lance_core::cache::CacheStats {
243        self.metadata_cache.0.stats().await
244    }
245
246    /// Fetch statistics for the index cache
247    pub async fn index_cache_stats(&self) -> lance_core::cache::CacheStats {
248        self.index_cache.0.stats().await
249    }
250}
251
252impl Default for Session {
253    fn default() -> Self {
254        Self::new(
255            DEFAULT_INDEX_CACHE_SIZE,
256            DEFAULT_METADATA_CACHE_SIZE,
257            Arc::new(ObjectStoreRegistry::default()),
258        )
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265    use lance_core::cache::UnsizedCacheKey;
266    use lance_index::vector::VectorIndex;
267    use std::borrow::Cow;
268    use tokio::io::AsyncWriteExt;
269
270    struct TestUnsizedKey(&'static str);
271    impl UnsizedCacheKey for TestUnsizedKey {
272        type ValueType = dyn VectorIndex;
273        fn key(&self) -> Cow<'_, str> {
274            Cow::Borrowed(self.0)
275        }
276
277        fn type_name() -> &'static str {
278            "TestUnsized"
279        }
280    }
281
282    #[tokio::test]
283    async fn test_disable_index_cache() {
284        let no_cache = Session::new(0, 0, Default::default());
285        assert!(
286            no_cache
287                .index_cache
288                .get_unsized_with_key(&TestUnsizedKey("abc"))
289                .await
290                .is_none()
291        );
292    }
293
294    #[tokio::test]
295    async fn test_default_session_has_spill_store() {
296        let session = Session::default();
297        // Should be able to allocate a spill and write to it without error.
298        let (mut writer, _spill) = session.spill_store().new_spill().await.unwrap();
299        writer.write_all(b"scratch").await.unwrap();
300        lance_io::traits::Writer::shutdown(writer.as_mut())
301            .await
302            .unwrap();
303    }
304
305    #[tokio::test]
306    async fn test_custom_spill_store_injected() {
307        let capped = Arc::new(LocalSpillStore::with_cap(50).unwrap());
308        let session = Session::default().with_spill_store(capped);
309
310        let (mut writer, _spill) = session.spill_store().new_spill().await.unwrap();
311        // Writing 51 bytes exceeds the 50-byte cap; the typed error is wrapped
312        // in an io::Error by the writer and recovered on conversion.
313        let io_err = writer.write_all(&[0u8; 51]).await.unwrap_err();
314        let err: lance_core::Error = io_err.into();
315        assert!(
316            matches!(
317                err,
318                lance_core::Error::DiskCapExceeded { cap_bytes: 50, .. }
319            ),
320            "expected DiskCapExceeded, got {err}"
321        );
322    }
323}