Skip to main content

summa_core/index/
reader.rs

1//! IndexReader - manages Searcher with reload policy (native only)
2//!
3//! The IndexReader periodically reloads its Searcher to pick up new segments.
4//! Uses SegmentManager as authoritative source for segment state.
5
6use std::sync::Arc;
7use std::sync::atomic::{AtomicBool, Ordering};
8
9use arc_swap::ArcSwap;
10use parking_lot::RwLock;
11
12use crate::directories::DirectoryWriter;
13use crate::dsl::Schema;
14use crate::error::Result;
15
16use super::Searcher;
17use super::searcher::SearcherResources;
18
19/// IndexReader - manages Searcher with reload policy
20///
21/// The IndexReader periodically reloads its Searcher to pick up new segments.
22/// Uses SegmentManager as authoritative source for segment state (avoids race conditions).
23/// Combined searcher + segment IDs, swapped atomically via ArcSwap (wait-free reads).
24struct SearcherState<D: DirectoryWriter + 'static> {
25    searcher: Arc<Searcher<D>>,
26    segment_ids: Vec<String>,
27    publication_id: u64,
28}
29
30/// Cancellation-safe ownership of the reload flag. Async reload checks may be
31/// dropped at any await point; resetting manually only on normal return leaves
32/// every future reload disabled after request cancellation or panic.
33struct ReloadGuard<'a>(&'a AtomicBool);
34
35impl Drop for ReloadGuard<'_> {
36    fn drop(&mut self) {
37        self.0.store(false, Ordering::Release);
38    }
39}
40
41pub struct IndexReader<D: DirectoryWriter + 'static> {
42    /// Schema
43    schema: Arc<Schema>,
44    /// Segment manager - authoritative source for segments
45    segment_manager: Arc<crate::merge::SegmentManager<D>>,
46    /// Current searcher + segment IDs (ArcSwap for wait-free reads)
47    state: ArcSwap<SearcherState<D>>,
48    /// Cache and CPU policy preserved across every searcher reload.
49    resources: SearcherResources,
50    /// Last reload check time
51    last_reload_check: RwLock<std::time::Instant>,
52    /// Reload check interval (default 1 second)
53    reload_check_interval: std::time::Duration,
54    /// Guard against concurrent reloads
55    reloading: AtomicBool,
56}
57
58impl<D: DirectoryWriter + 'static> IndexReader<D> {
59    /// Create a new IndexReader from a segment manager
60    ///
61    /// Centroids are loaded dynamically from metadata on each reload,
62    /// so the reader always picks up centroids trained after Index::create().
63    pub async fn from_segment_manager(
64        schema: Arc<Schema>,
65        segment_manager: Arc<crate::merge::SegmentManager<D>>,
66        term_cache_blocks: usize,
67        reload_interval_ms: u64,
68    ) -> Result<Self> {
69        const STANDALONE_STORE_CACHE_BYTES: usize = 32 * 1024 * 1024;
70        let resources = SearcherResources::new(
71            term_cache_blocks,
72            None,
73            STANDALONE_STORE_CACHE_BYTES,
74            crate::default_search_threads(),
75            4,
76        )?;
77        Self::from_segment_manager_with_resources(
78            schema,
79            segment_manager,
80            reload_interval_ms,
81            resources,
82        )
83        .await
84    }
85
86    /// Internal constructor used by `Index` to preserve its configured cache
87    /// and search CPU policy across reader reloads.
88    pub(crate) async fn from_segment_manager_with_resources(
89        schema: Arc<Schema>,
90        segment_manager: Arc<crate::merge::SegmentManager<D>>,
91        reload_interval_ms: u64,
92        resources: SearcherResources,
93    ) -> Result<Self> {
94        // Get initial segment IDs
95        let initial_segment_ids = segment_manager.get_segment_ids().await;
96
97        let (reader, publication_id) =
98            Self::create_reader(&schema, &segment_manager, resources.clone()).await?;
99
100        Ok(Self {
101            schema,
102            segment_manager,
103            state: ArcSwap::from_pointee(SearcherState {
104                searcher: Arc::new(reader),
105                segment_ids: initial_segment_ids,
106                publication_id,
107            }),
108            resources,
109            last_reload_check: RwLock::new(std::time::Instant::now()),
110            reload_check_interval: std::time::Duration::from_millis(reload_interval_ms),
111            reloading: AtomicBool::new(false),
112        })
113    }
114
115    /// Create a new reader with fresh snapshot from segment manager
116    ///
117    /// Captures segment IDs and their trained vector generation together.
118    async fn create_reader(
119        schema: &Arc<Schema>,
120        segment_manager: &Arc<crate::merge::SegmentManager<D>>,
121        resources: SearcherResources,
122    ) -> Result<(Searcher<D>, u64)> {
123        let snapshot = segment_manager.acquire_snapshot().await;
124        let generation = snapshot.published_generation();
125        let snapshot_schema = generation
126            .as_ref()
127            .map(|generation| Arc::clone(&generation.schema))
128            .unwrap_or_else(|| Arc::clone(schema));
129        let trained = generation
130            .as_ref()
131            .and_then(|generation| generation.trained_vectors.clone())
132            .unwrap_or_else(|| Arc::new(crate::segment::TrainedVectorStructures::default()));
133        let publication_id = generation
134            .as_ref()
135            .map_or(0, |generation| generation.publication_id);
136
137        let searcher = Searcher::from_snapshot(
138            segment_manager.directory(),
139            snapshot_schema,
140            snapshot,
141            trained,
142            resources,
143        )
144        .await?;
145        Ok((searcher, publication_id))
146    }
147
148    /// Set reload check interval
149    pub fn set_reload_interval(&mut self, interval: std::time::Duration) {
150        self.reload_check_interval = interval;
151    }
152
153    /// Get current searcher (reloads only if segments changed)
154    ///
155    /// Wait-free read path via ArcSwap::load(). Reload checks are guarded
156    /// by an AtomicBool to prevent concurrent reloads.
157    pub async fn searcher(&self) -> Result<Arc<Searcher<D>>> {
158        // Check if we should check for segment changes
159        let should_check = {
160            let last = self.last_reload_check.read();
161            last.elapsed() >= self.reload_check_interval
162        };
163
164        if should_check {
165            // Try to acquire the reload guard (non-blocking)
166            if self
167                .reloading
168                .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
169                .is_ok()
170            {
171                let _reload_guard = ReloadGuard(&self.reloading);
172                // We won the race — do the reload check
173                self.do_reload_check().await?;
174            }
175            // Otherwise another reload is in progress — just return current searcher
176        }
177
178        // Wait-free load (no lock contention with reloads)
179        Ok(Arc::clone(&self.state.load().searcher))
180    }
181
182    /// Actual reload check (called under the `reloading` guard)
183    async fn do_reload_check(&self) -> Result<()> {
184        *self.last_reload_check.write() = std::time::Instant::now();
185
186        // Get current segment IDs from segment manager
187        let new_segment_ids = self.segment_manager.get_segment_ids().await;
188
189        // Check if segments actually changed (wait-free read)
190        let publication_id = self.segment_manager.publication_id();
191        let generation_changed = {
192            let state = self.state.load();
193            state.segment_ids != new_segment_ids || state.publication_id != publication_id
194        };
195
196        if generation_changed {
197            let old_count = self.state.load().segment_ids.len();
198            let new_count = new_segment_ids.len();
199            log::info!(
200                "[index_reload] index={} old_count={} new_count={}",
201                self.schema.index_label(),
202                old_count,
203                new_count
204            );
205            self.reload_with_segments(new_segment_ids).await?;
206        }
207        Ok(())
208    }
209
210    /// Force reload reader with fresh snapshot.
211    ///
212    /// Waits for any in-progress reload (from `searcher()`) to finish, then
213    /// performs its own reload with the latest segment IDs. This guarantees
214    /// the reload actually happens — unlike `searcher()` which silently skips
215    /// if another reload is in progress.
216    pub async fn reload(&self) -> Result<()> {
217        // Wait for any in-progress reload to finish, then acquire the guard.
218        // This is critical: a concurrent do_reload_check() may have started
219        // before a commit, so its reload won't see the new segments.
220        loop {
221            if self
222                .reloading
223                .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
224                .is_ok()
225            {
226                break;
227            }
228            tokio::task::yield_now().await;
229        }
230        let _reload_guard = ReloadGuard(&self.reloading);
231        let new_segment_ids = self.segment_manager.get_segment_ids().await;
232
233        // Fast path: skip reload if segments haven't changed
234        let publication_id = self.segment_manager.publication_id();
235        let generation_changed = {
236            let state = self.state.load();
237            state.segment_ids != new_segment_ids || state.publication_id != publication_id
238        };
239
240        if generation_changed {
241            self.reload_with_segments(new_segment_ids).await
242        } else {
243            log::debug!(
244                "[reload] index={} segments unchanged, skipping",
245                self.schema.index_label()
246            );
247            Ok(())
248        }
249    }
250
251    /// Internal reload with specific segment IDs.
252    /// Reuses existing segment readers for unchanged segments (avoids re-opening
253    /// mmaps, fast fields, sparse indexes, etc.).
254    /// Atomic swap via ArcSwap::store (wait-free for readers).
255    async fn reload_with_segments(&self, new_segment_ids: Vec<String>) -> Result<()> {
256        // Collect existing segment readers for reuse
257        let existing_segments: Vec<Arc<crate::segment::SegmentReader>> =
258            self.state.load().searcher.segment_readers().to_vec();
259
260        let snapshot = self.segment_manager.acquire_snapshot().await;
261        let generation = snapshot.published_generation();
262        let schema = generation
263            .as_ref()
264            .map(|generation| Arc::clone(&generation.schema))
265            .unwrap_or_else(|| Arc::clone(&self.schema));
266        let trained = generation
267            .as_ref()
268            .and_then(|generation| generation.trained_vectors.clone())
269            .unwrap_or_else(|| Arc::new(crate::segment::TrainedVectorStructures::default()));
270        let publication_id = generation
271            .as_ref()
272            .map_or(0, |generation| generation.publication_id);
273
274        let new_reader = Searcher::from_snapshot_reuse(
275            self.segment_manager.directory(),
276            schema,
277            snapshot,
278            trained,
279            self.resources.clone(),
280            &existing_segments,
281        )
282        .await?;
283
284        // Atomic swap — readers see old or new state, never a torn read
285        self.state.store(Arc::new(SearcherState {
286            searcher: Arc::new(new_reader),
287            segment_ids: new_segment_ids,
288            publication_id,
289        }));
290
291        Ok(())
292    }
293
294    /// Get schema
295    pub fn schema(&self) -> Arc<Schema> {
296        self.schema_arc()
297    }
298
299    /// Schema of the currently published search generation.
300    pub fn schema_arc(&self) -> Arc<Schema> {
301        self.state.load().searcher.schema_arc()
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn reload_guard_releases_flag_on_unwind() {
311        let reloading = AtomicBool::new(true);
312        let result = std::panic::catch_unwind(|| {
313            let _guard = ReloadGuard(&reloading);
314            panic!("cancel reload");
315        });
316        assert!(result.is_err());
317        assert!(!reloading.load(Ordering::Acquire));
318    }
319
320    #[tokio::test]
321    async fn schema_publication_is_atomic_for_old_and_new_searchers() {
322        use crate::directories::RamDirectory;
323        use crate::dsl::{DenseVectorConfig, SchemaBuilder, VectorIndexAlter, VectorIndexType};
324
325        let mut builder = SchemaBuilder::default();
326        let field = builder.add_dense_vector_field_with_config(
327            "embedding",
328            true,
329            true,
330            DenseVectorConfig::ivf_tq(4, Some(2), 1),
331        );
332        let directory = RamDirectory::new();
333        let index = crate::Index::create(
334            directory.clone(),
335            builder.build(),
336            crate::IndexConfig::default(),
337        )
338        .await
339        .unwrap();
340        let reader = index.reader().await.unwrap();
341        let old_searcher = reader.searcher().await.unwrap();
342
343        let mut target = DenseVectorConfig::ivf_tq(4, Some(2), 1);
344        target.index_type = VectorIndexType::Scann;
345        target.tree_levels = Some(1);
346        target.soar = None;
347        let next_schema = Arc::new(
348            index
349                .schema_arc()
350                .with_vector_index_alter(field, VectorIndexAlter::Dense(target))
351                .unwrap(),
352        );
353        let update = index
354            .segment_manager()
355            .begin_vector_artifact_update()
356            .await
357            .unwrap();
358        index
359            .segment_manager()
360            .publish_vector_schema_only(&update, next_schema)
361            .await
362            .unwrap();
363        drop(update);
364
365        assert_eq!(
366            old_searcher
367                .schema()
368                .get_field_entry(field)
369                .unwrap()
370                .dense_vector_config
371                .as_ref()
372                .unwrap()
373                .index_type,
374            VectorIndexType::IvfTq
375        );
376        reader.reload().await.unwrap();
377        let new_searcher = reader.searcher().await.unwrap();
378        assert_eq!(
379            new_searcher
380                .schema()
381                .get_field_entry(field)
382                .unwrap()
383                .dense_vector_config
384                .as_ref()
385                .unwrap()
386                .index_type,
387            VectorIndexType::Scann
388        );
389
390        let reopened = crate::Index::open(directory, crate::IndexConfig::default())
391            .await
392            .unwrap();
393        assert_eq!(
394            reopened
395                .schema_arc()
396                .get_field_entry(field)
397                .unwrap()
398                .dense_vector_config
399                .as_ref()
400                .unwrap()
401                .index_type,
402            VectorIndexType::Scann
403        );
404    }
405}