use std::borrow::Cow;
use std::future::Future;
use std::sync::{Arc, Mutex, OnceLock};
use arc_swap::ArcSwap;
use arrow_schema::{DataType, Field};
use async_trait::async_trait;
use datafusion::execution::SendableRecordBatchStream;
use futures::future::BoxFuture;
use lance_core::{
Result,
cache::{CacheKey, CacheKeySchema, KeyBuilder, LanceCache, UnsizedCacheKey},
deepsize::{Context, DeepSizeOf},
};
use crate::progress::IndexBuildProgress;
use crate::registry::IndexPluginRegistry;
use crate::scalar::RowIdRemapper;
use crate::scalar::{CreatedIndex, IndexStore, ScalarIndex, expression::ScalarQueryParser};
pub use crate::scalar::{TrainingCriteria, TrainingOrdering};
pub const VALUE_COLUMN_NAME: &str = "value";
pub trait TrainingRequest: std::any::Any + Send + Sync {
fn as_any(&self) -> &dyn std::any::Any;
fn criteria(&self) -> &TrainingCriteria;
}
pub(crate) struct DefaultTrainingRequest {
criteria: TrainingCriteria,
}
impl DefaultTrainingRequest {
pub fn new(criteria: TrainingCriteria) -> Self {
Self { criteria }
}
}
impl TrainingRequest for DefaultTrainingRequest {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn criteria(&self) -> &TrainingCriteria {
&self.criteria
}
}
#[async_trait]
pub trait BasicTrainer: Send + Sync {
fn new_training_request(&self, params: &str, field: &Field)
-> Result<Box<dyn TrainingRequest>>;
async fn train_index(
&self,
data: SendableRecordBatchStream,
index_store: &dyn IndexStore,
request: Box<dyn TrainingRequest>,
fragment_ids: Option<Vec<u32>>,
progress: Arc<dyn IndexBuildProgress>,
) -> Result<CreatedIndex>;
}
#[async_trait]
pub trait ScalarIndexPlugin: Send + Sync + std::fmt::Debug {
fn basic_trainer(&self) -> Option<&dyn BasicTrainer> {
None
}
fn name(&self) -> &str;
fn provides_exact_answer(&self) -> bool;
fn version(&self) -> u32;
fn new_query_parser(
&self,
index_name: String,
index_details: &prost_types::Any,
) -> Option<Box<dyn ScalarQueryParser>>;
async fn load_index(
&self,
index_store: Arc<dyn IndexStore>,
index_details: &prost_types::Any,
frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
cache: &LanceCache,
) -> Result<Arc<dyn ScalarIndex>>;
async fn get_from_cache(
&self,
index_store: Arc<dyn IndexStore>,
_frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
cache: &LanceCache,
) -> Result<Option<Arc<dyn ScalarIndex>>> {
let Some(entry) = cache.get_unsized_with_key(&ScalarIndexCacheKey).await else {
return Ok(None);
};
Ok(entry.index_for_store(&index_store))
}
async fn put_in_cache(
&self,
index_store: Arc<dyn IndexStore>,
cache: &LanceCache,
index: Arc<dyn ScalarIndex>,
) -> Result<()> {
cache
.insert_unsized_with_key(
&ScalarIndexCacheKey,
Arc::new(StoreBoundScalarIndexCacheEntry::new(index_store, index)),
)
.await;
Ok(())
}
async fn get_or_insert_in_cache(
&self,
index_store: Arc<dyn IndexStore>,
frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
cache: &LanceCache,
load: ScalarIndexLoad<'_>,
) -> Result<Arc<dyn ScalarIndex>> {
if let Some(index) = self
.get_from_cache(index_store.clone(), frag_reuse_index, cache)
.await?
{
return Ok(index);
}
let index = load.await?;
self.put_in_cache(index_store, cache, index.clone()).await?;
Ok(index)
}
async fn load_statistics(
&self,
_index_store: Arc<dyn IndexStore>,
_index_details: &prost_types::Any,
) -> Result<Option<serde_json::Value>> {
Ok(None)
}
fn attach_registry(&self, _registry: Arc<IndexPluginRegistry>) {}
fn details_as_json(&self, _details: &prost_types::Any) -> Result<serde_json::Value> {
Ok(serde_json::json!({}))
}
async fn create_seed_writer(
&self,
_field_path: &str,
_data_type: &DataType,
_index_details: &prost_types::Any,
) -> Result<Option<Box<dyn super::seed::IndexSeedWriter>>> {
Ok(None)
}
fn might_use_seeds(&self, _index_details: &prost_types::Any) -> bool {
false
}
async fn update_from_seeds(
&self,
_seeds: Vec<super::seed::FragmentSeed>,
_reference_index: Arc<dyn ScalarIndex>,
_index_details: &prost_types::Any,
_dest_store: &dyn IndexStore,
) -> Result<Option<CreatedIndex>> {
Ok(None)
}
}
pub type ScalarIndexLoad<'a> = BoxFuture<'a, Result<Arc<dyn ScalarIndex>>>;
pub async fn single_flight_open<K, ToState, FromState>(
cache: &LanceCache,
state_key: K,
load: ScalarIndexLoad<'_>,
to_state: ToState,
from_state: FromState,
) -> Result<Arc<dyn ScalarIndex>>
where
K: CacheKey + Send,
K::ValueType: DeepSizeOf + Send + Sync + 'static,
ToState: FnOnce(&dyn ScalarIndex) -> Result<K::ValueType> + Send,
FromState: FnOnce(Arc<K::ValueType>) -> Result<Arc<dyn ScalarIndex>> + Send,
{
let state = cache
.get_or_insert_with_key(state_key, move || async move {
let index = load.await?;
to_state(index.as_ref())
})
.await?;
from_state(state)
}
pub(crate) async fn single_flight_store_bound_open<Rebind, RebindFuture>(
index_store: Arc<dyn IndexStore>,
cache: &LanceCache,
load: ScalarIndexLoad<'_>,
rebind: Rebind,
) -> Result<Arc<dyn ScalarIndex>>
where
Rebind: FnOnce(Arc<dyn ScalarIndex>) -> RebindFuture + Send,
RebindFuture: Future<Output = Result<Option<Arc<dyn ScalarIndex>>>> + Send,
{
let pending_load = Arc::new(Mutex::new(Some(load)));
let cache_load = pending_load.clone();
let loaded_index = Arc::new(OnceLock::new());
let cache_loaded_index = loaded_index.clone();
let cache_index_store = index_store.clone();
let entry = cache
.get_or_insert_unsized_with_key(ScalarIndexCacheKey, move || async move {
let load = take_scalar_index_load(&cache_load)?.ok_or_else(|| {
lance_core::Error::internal(
"store-bound scalar index cache loader was already consumed",
)
})?;
let index = load.await?;
cache_loaded_index.get_or_init(|| index.clone());
Ok(Arc::new(StoreBoundScalarIndexCacheEntry::new(
cache_index_store,
index,
)))
})
.await?;
if let Some(index) = loaded_index.get() {
return Ok(index.clone());
}
let binding = entry.binding.load_full();
if index_store.is_same_storage_binding(binding.index_store.as_ref()) {
return Ok(binding.index.clone());
}
if let Some(index) = rebind(binding.index.clone()).await? {
let previous = entry.binding.compare_and_swap(
&binding,
Arc::new(StoreBoundScalarIndexBinding {
index_store: index_store.clone(),
index: index.clone(),
}),
);
if !Arc::ptr_eq(&previous, &binding)
&& index_store.is_same_storage_binding(previous.index_store.as_ref())
{
return Ok(previous.index.clone());
}
return Ok(index);
}
let _replacement_guard = entry.replacement_guard.lock().await;
if let Some(index) = entry.index_for_store(&index_store) {
return Ok(index);
}
let load = take_scalar_index_load(&pending_load)?.ok_or_else(|| {
lance_core::Error::internal("store-bound scalar index load has no retained result")
})?;
let index = load.await?;
entry.replace(index_store, index.clone());
Ok(index)
}
fn take_scalar_index_load<'a>(
pending_load: &Arc<Mutex<Option<ScalarIndexLoad<'a>>>>,
) -> Result<Option<ScalarIndexLoad<'a>>> {
pending_load
.lock()
.map_err(|_| {
lance_core::Error::internal("store-bound scalar index cache loader mutex was poisoned")
})
.map(|mut pending_load| pending_load.take())
}
#[derive(DeepSizeOf)]
struct StoreBoundScalarIndexBinding {
index_store: Arc<dyn IndexStore>,
index: Arc<dyn ScalarIndex>,
}
pub struct StoreBoundScalarIndexCacheEntry {
binding: ArcSwap<StoreBoundScalarIndexBinding>,
replacement_guard: tokio::sync::Mutex<()>,
}
impl DeepSizeOf for StoreBoundScalarIndexCacheEntry {
fn deep_size_of_children(&self, context: &mut Context) -> usize {
self.binding.load_full().deep_size_of_children(context)
}
}
impl StoreBoundScalarIndexCacheEntry {
fn new(index_store: Arc<dyn IndexStore>, index: Arc<dyn ScalarIndex>) -> Self {
Self {
binding: ArcSwap::from_pointee(StoreBoundScalarIndexBinding { index_store, index }),
replacement_guard: tokio::sync::Mutex::new(()),
}
}
fn index_for_store(&self, index_store: &Arc<dyn IndexStore>) -> Option<Arc<dyn ScalarIndex>> {
let binding = self.binding.load();
index_store
.is_same_storage_binding(binding.index_store.as_ref())
.then(|| binding.index.clone())
}
fn replace(&self, index_store: Arc<dyn IndexStore>, index: Arc<dyn ScalarIndex>) {
self.binding.store(Arc::new(StoreBoundScalarIndexBinding {
index_store,
index,
}));
}
pub fn index(&self) -> Arc<dyn ScalarIndex> {
self.binding.load().index.clone()
}
}
pub struct ScalarIndexCacheKey;
impl UnsizedCacheKey for ScalarIndexCacheKey {
type ValueType = StoreBoundScalarIndexCacheEntry;
fn key(&self) -> Cow<'_, str> {
Cow::Borrowed("scalar_index")
}
fn type_name() -> &'static str {
"ScalarIndex"
}
fn schema() -> CacheKeySchema {
CacheKeySchema::new("lance.scalar.registry.scalar-index-key", 2)
}
fn write_key(&self, builder: &mut KeyBuilder) {
builder.write_variant(0);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{collections::HashMap, pin::Pin};
use arrow_schema::Schema;
use futures::FutureExt;
use lance_core::cache::{
CacheBackend, CacheCodec, CacheEntry, InternalCacheKey, MokaCacheBackend,
};
use lance_io::object_store::ObjectStore;
use tokio::sync::Notify;
use crate::scalar::inverted::{
InvertedIndex, InvertedIndexParams, METADATA_FILE, TOKEN_SET_FORMAT_KEY, TokenSetFormat,
};
use crate::scalar::lance_format::LanceIndexStore;
#[derive(Debug)]
struct PauseColdReturn {
inner: MokaCacheBackend,
published: Notify,
resume: Notify,
}
#[async_trait]
impl CacheBackend for PauseColdReturn {
async fn get(
&self,
key: &InternalCacheKey,
codec: Option<CacheCodec>,
) -> Option<CacheEntry> {
self.inner.get(key, codec).await
}
async fn insert(
&self,
key: &InternalCacheKey,
entry: CacheEntry,
size_bytes: usize,
codec: Option<CacheCodec>,
) {
self.inner.insert(key, entry, size_bytes, codec).await;
}
async fn get_or_insert<'a>(
&self,
key: &InternalCacheKey,
loader: Pin<Box<dyn Future<Output = Result<(CacheEntry, usize)>> + Send + 'a>>,
codec: Option<CacheCodec>,
) -> Result<(CacheEntry, bool)> {
let result = self.inner.get_or_insert(key, loader, codec).await?;
if !result.1 {
self.published.notify_one();
self.resume.notified().await;
}
Ok(result)
}
async fn clear(&self) {
self.inner.clear().await;
}
async fn num_entries(&self) -> usize {
self.inner.num_entries().await
}
async fn size_bytes(&self) -> usize {
self.inner.size_bytes().await
}
}
async fn empty_index_bindings() -> [(Arc<dyn IndexStore>, Arc<dyn ScalarIndex>); 2] {
let object_store = ObjectStore::memory();
let metadata_cache = Arc::new(LanceCache::with_capacity(1024 * 1024));
let store_a: Arc<dyn IndexStore> = Arc::new(LanceIndexStore::new(
Arc::new(object_store.clone()),
"index".into(),
metadata_cache.clone(),
));
let store_b: Arc<dyn IndexStore> = Arc::new(LanceIndexStore::new(
Arc::new(object_store),
"index".into(),
metadata_cache,
));
assert!(!store_a.is_same_storage_binding(store_b.as_ref()));
let mut writer = store_a
.new_index_file(METADATA_FILE, Arc::new(Schema::empty()))
.await
.unwrap();
writer
.finish_with_metadata(HashMap::from([
("partitions".to_owned(), "[]".to_owned()),
(
"params".to_owned(),
serde_json::to_string(&InvertedIndexParams::default()).unwrap(),
),
(
TOKEN_SET_FORMAT_KEY.to_owned(),
TokenSetFormat::default().to_string(),
),
]))
.await
.unwrap();
let index_cache = LanceCache::no_cache();
let index_a = InvertedIndex::load(store_a.clone(), None, &index_cache)
.await
.unwrap();
let index_b = InvertedIndex::load(store_b.clone(), None, &index_cache)
.await
.unwrap();
[(store_a, index_a), (store_b, index_b)]
}
#[tokio::test]
async fn test_cold_scalar_open_keeps_its_binding_after_concurrent_rotation() {
let [(store_a, index_a), (store_b, index_b)] = empty_index_bindings().await;
let backend = Arc::new(PauseColdReturn {
inner: MokaCacheBackend::with_capacity(1024 * 1024),
published: Notify::new(),
resume: Notify::new(),
});
let cache = LanceCache::with_backend(backend.clone());
let cold_cache = cache.clone();
let cold_index = index_a.clone();
let cold = tokio::spawn(async move {
single_flight_store_bound_open(
store_a,
&cold_cache,
async move { Ok(cold_index) }.boxed(),
|_| async { panic!("cold caller must keep its own loaded index") },
)
.await
});
backend.published.notified().await;
let replacement = index_b.clone();
let warm = single_flight_store_bound_open(
store_b,
&cache,
async { panic!("warm caller must rebind the cached index") }.boxed(),
|_| async move { Ok(Some(replacement)) },
)
.await
.unwrap();
assert!(Arc::ptr_eq(&warm, &index_b));
backend.resume.notify_one();
let cold = cold.await.unwrap().unwrap();
assert!(Arc::ptr_eq(&cold, &index_a));
let cached = cache
.get_unsized_with_key(&ScalarIndexCacheKey)
.await
.unwrap();
assert!(Arc::ptr_eq(&cached.index(), &index_b));
}
#[tokio::test]
async fn test_independent_scalar_rebinds_do_not_wait_for_each_other() {
let [(store_a, index_a), (store_b, index_b)] = empty_index_bindings().await;
let [(store_c, index_c), _] = empty_index_bindings().await;
let cache = LanceCache::with_capacity(1024 * 1024);
let entry = Arc::new(StoreBoundScalarIndexCacheEntry::new(store_a, index_a));
cache
.insert_unsized_with_key(&ScalarIndexCacheKey, entry.clone())
.await;
let started = Arc::new(Notify::new());
let resume = Arc::new(Notify::new());
let slow_started = started.clone();
let slow_resume = resume.clone();
let slow_cache = cache.clone();
let slow_index = index_b.clone();
let slow = tokio::spawn(async move {
single_flight_store_bound_open(
store_b,
&slow_cache,
async { panic!("warm request must not reload metadata") }.boxed(),
|_| async move {
slow_started.notify_one();
slow_resume.notified().await;
Ok(Some(slow_index))
},
)
.await
});
started.notified().await;
let fast_index = index_c.clone();
let fast = tokio::time::timeout(
std::time::Duration::from_millis(500),
single_flight_store_bound_open(
store_c,
&cache,
async { panic!("warm request must not reload metadata") }.boxed(),
|_| async move { Ok(Some(fast_index)) },
),
)
.await;
resume.notify_one();
let slow = slow.await.unwrap().unwrap();
let fast = fast
.expect("an independent request waited for another binding's rebind")
.unwrap();
assert!(Arc::ptr_eq(&slow, &index_b));
assert!(Arc::ptr_eq(&fast, &index_c));
assert!(
Arc::ptr_eq(&entry.index(), &index_c),
"a delayed rebind must not overwrite the newer cache binding"
);
}
#[tokio::test]
async fn test_failed_or_cancelled_scalar_rebind_preserves_cached_binding() {
let [(store_a, index_a), (store_b, index_b)] = empty_index_bindings().await;
let cache = LanceCache::with_capacity(1024 * 1024);
let entry = Arc::new(StoreBoundScalarIndexCacheEntry::new(
store_a,
index_a.clone(),
));
cache
.insert_unsized_with_key(&ScalarIndexCacheKey, entry.clone())
.await;
let error = single_flight_store_bound_open(
store_b.clone(),
&cache,
async { panic!("failed rebind must not fall back to loading") }.boxed(),
|_| async { Err(lance_core::Error::io("replacement credentials revoked")) },
)
.await
.unwrap_err();
assert!(matches!(error, lance_core::Error::IO { .. }));
assert!(
error
.to_string()
.contains("replacement credentials revoked")
);
assert!(Arc::ptr_eq(&entry.index(), &index_a));
let started = Arc::new(Notify::new());
let cancel_started = started.clone();
let cancel_cache = cache.clone();
let cancel_store = store_b.clone();
let cancelled = tokio::spawn(async move {
single_flight_store_bound_open(
cancel_store,
&cancel_cache,
async { panic!("cancelled rebind must not fall back to loading") }.boxed(),
|_| async move {
cancel_started.notify_one();
futures::future::pending().await
},
)
.await
});
started.notified().await;
cancelled.abort();
assert!(cancelled.await.unwrap_err().is_cancelled());
assert!(Arc::ptr_eq(&entry.index(), &index_a));
let replacement = index_b.clone();
let reopened = single_flight_store_bound_open(
store_b,
&cache,
async { panic!("retry must rebind the cached index") }.boxed(),
|_| async move { Ok(Some(replacement)) },
)
.await
.unwrap();
assert!(Arc::ptr_eq(&reopened, &index_b));
assert!(Arc::ptr_eq(&entry.index(), &index_b));
}
}