Skip to main content

hermes_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}
28
29/// Cancellation-safe ownership of the reload flag. Async reload checks may be
30/// dropped at any await point; resetting manually only on normal return leaves
31/// every future reload disabled after request cancellation or panic.
32struct ReloadGuard<'a>(&'a AtomicBool);
33
34impl Drop for ReloadGuard<'_> {
35    fn drop(&mut self) {
36        self.0.store(false, Ordering::Release);
37    }
38}
39
40pub struct IndexReader<D: DirectoryWriter + 'static> {
41    /// Schema
42    schema: Arc<Schema>,
43    /// Segment manager - authoritative source for segments
44    segment_manager: Arc<crate::merge::SegmentManager<D>>,
45    /// Current searcher + segment IDs (ArcSwap for wait-free reads)
46    state: ArcSwap<SearcherState<D>>,
47    /// Cache and CPU policy preserved across every searcher reload.
48    resources: SearcherResources,
49    /// Last reload check time
50    last_reload_check: RwLock<std::time::Instant>,
51    /// Reload check interval (default 1 second)
52    reload_check_interval: std::time::Duration,
53    /// Guard against concurrent reloads
54    reloading: AtomicBool,
55}
56
57impl<D: DirectoryWriter + 'static> IndexReader<D> {
58    /// Create a new IndexReader from a segment manager
59    ///
60    /// Centroids are loaded dynamically from metadata on each reload,
61    /// so the reader always picks up centroids trained after Index::create().
62    pub async fn from_segment_manager(
63        schema: Arc<Schema>,
64        segment_manager: Arc<crate::merge::SegmentManager<D>>,
65        term_cache_blocks: usize,
66        reload_interval_ms: u64,
67    ) -> Result<Self> {
68        Self::from_segment_manager_with_cache_blocks(
69            schema,
70            segment_manager,
71            term_cache_blocks,
72            term_cache_blocks,
73            reload_interval_ms,
74        )
75        .await
76    }
77
78    /// Create an IndexReader with independent term and document-store caches.
79    pub async fn from_segment_manager_with_cache_blocks(
80        schema: Arc<Schema>,
81        segment_manager: Arc<crate::merge::SegmentManager<D>>,
82        term_cache_blocks: usize,
83        store_cache_blocks: usize,
84        reload_interval_ms: u64,
85    ) -> Result<Self> {
86        let resources = SearcherResources::new(
87            term_cache_blocks,
88            store_cache_blocks,
89            crate::default_search_threads(),
90        )?;
91        Self::from_segment_manager_with_resources(
92            schema,
93            segment_manager,
94            reload_interval_ms,
95            resources,
96        )
97        .await
98    }
99
100    /// Internal constructor used by `Index` to preserve its configured cache
101    /// and search CPU policy across reader reloads.
102    pub(crate) async fn from_segment_manager_with_resources(
103        schema: Arc<Schema>,
104        segment_manager: Arc<crate::merge::SegmentManager<D>>,
105        reload_interval_ms: u64,
106        resources: SearcherResources,
107    ) -> Result<Self> {
108        // Get initial segment IDs
109        let initial_segment_ids = segment_manager.get_segment_ids().await;
110
111        let reader = Self::create_reader(&schema, &segment_manager, resources.clone()).await?;
112
113        Ok(Self {
114            schema,
115            segment_manager,
116            state: ArcSwap::from_pointee(SearcherState {
117                searcher: Arc::new(reader),
118                segment_ids: initial_segment_ids,
119            }),
120            resources,
121            last_reload_check: RwLock::new(std::time::Instant::now()),
122            reload_check_interval: std::time::Duration::from_millis(reload_interval_ms),
123            reloading: AtomicBool::new(false),
124        })
125    }
126
127    /// Create a new reader with fresh snapshot from segment manager
128    ///
129    /// Reads trained centroids from SegmentManager's ArcSwap (lock-free).
130    async fn create_reader(
131        schema: &Arc<Schema>,
132        segment_manager: &Arc<crate::merge::SegmentManager<D>>,
133        resources: SearcherResources,
134    ) -> Result<Searcher<D>> {
135        // Use SegmentManager's acquire_snapshot - non-blocking RwLock read.
136        //
137        // The snapshot MUST be acquired before reading trained centroids:
138        // segment producers capture trained structures only after the ArcSwap
139        // publication, so any ANN segment visible in the snapshot is always
140        // satisfiable by a trained value loaded after the snapshot. The
141        // reverse order can leave an ANN segment without centroids — and the
142        // miss is sticky, because reused readers are never re-injected on
143        // later reloads.
144        let snapshot = segment_manager.acquire_snapshot().await;
145
146        // Read one immutable trained-artifact generation from ArcSwap.
147        let trained = segment_manager
148            .trained()
149            .unwrap_or_else(|| Arc::new(crate::segment::TrainedVectorStructures::default()));
150
151        Searcher::from_snapshot(
152            segment_manager.directory(),
153            Arc::clone(schema),
154            snapshot,
155            trained,
156            resources,
157        )
158        .await
159    }
160
161    /// Set reload check interval
162    pub fn set_reload_interval(&mut self, interval: std::time::Duration) {
163        self.reload_check_interval = interval;
164    }
165
166    /// Get current searcher (reloads only if segments changed)
167    ///
168    /// Wait-free read path via ArcSwap::load(). Reload checks are guarded
169    /// by an AtomicBool to prevent concurrent reloads.
170    pub async fn searcher(&self) -> Result<Arc<Searcher<D>>> {
171        // Check if we should check for segment changes
172        let should_check = {
173            let last = self.last_reload_check.read();
174            last.elapsed() >= self.reload_check_interval
175        };
176
177        if should_check {
178            // Try to acquire the reload guard (non-blocking)
179            if self
180                .reloading
181                .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
182                .is_ok()
183            {
184                let _reload_guard = ReloadGuard(&self.reloading);
185                // We won the race — do the reload check
186                self.do_reload_check().await?;
187            }
188            // Otherwise another reload is in progress — just return current searcher
189        }
190
191        // Wait-free load (no lock contention with reloads)
192        Ok(Arc::clone(&self.state.load().searcher))
193    }
194
195    /// Actual reload check (called under the `reloading` guard)
196    async fn do_reload_check(&self) -> Result<()> {
197        *self.last_reload_check.write() = std::time::Instant::now();
198
199        // Get current segment IDs from segment manager
200        let new_segment_ids = self.segment_manager.get_segment_ids().await;
201
202        // Check if segments actually changed (wait-free read)
203        let segments_changed = {
204            let state = self.state.load();
205            state.segment_ids != new_segment_ids
206        };
207
208        if segments_changed {
209            let old_count = self.state.load().segment_ids.len();
210            let new_count = new_segment_ids.len();
211            log::info!(
212                "[index_reload] old_count={} new_count={}",
213                old_count,
214                new_count
215            );
216            self.reload_with_segments(new_segment_ids).await?;
217        }
218        Ok(())
219    }
220
221    /// Force reload reader with fresh snapshot.
222    ///
223    /// Waits for any in-progress reload (from `searcher()`) to finish, then
224    /// performs its own reload with the latest segment IDs. This guarantees
225    /// the reload actually happens — unlike `searcher()` which silently skips
226    /// if another reload is in progress.
227    pub async fn reload(&self) -> Result<()> {
228        // Wait for any in-progress reload to finish, then acquire the guard.
229        // This is critical: a concurrent do_reload_check() may have started
230        // before a commit, so its reload won't see the new segments.
231        loop {
232            if self
233                .reloading
234                .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
235                .is_ok()
236            {
237                break;
238            }
239            tokio::task::yield_now().await;
240        }
241        let _reload_guard = ReloadGuard(&self.reloading);
242        let new_segment_ids = self.segment_manager.get_segment_ids().await;
243
244        // Fast path: skip reload if segments haven't changed
245        let segments_changed = {
246            let state = self.state.load();
247            state.segment_ids != new_segment_ids
248        };
249
250        if segments_changed {
251            self.reload_with_segments(new_segment_ids).await
252        } else {
253            log::debug!("[reload] segments unchanged, skipping");
254            Ok(())
255        }
256    }
257
258    /// Internal reload with specific segment IDs.
259    /// Reuses existing segment readers for unchanged segments (avoids re-opening
260    /// mmaps, fast fields, sparse indexes, etc.).
261    /// Atomic swap via ArcSwap::store (wait-free for readers).
262    async fn reload_with_segments(&self, new_segment_ids: Vec<String>) -> Result<()> {
263        // Collect existing segment readers for reuse
264        let existing_segments: Vec<Arc<crate::segment::SegmentReader>> =
265            self.state.load().searcher.segment_readers().to_vec();
266
267        // Acquire the snapshot BEFORE reading trained centroids: producers
268        // capture trained structures only after the ArcSwap publication, so
269        // any ANN segment visible in the snapshot is always satisfiable by a
270        // trained value loaded after the snapshot (see create_reader).
271        let snapshot = self.segment_manager.acquire_snapshot().await;
272
273        // Read one immutable trained-artifact generation from ArcSwap.
274        let trained = self
275            .segment_manager
276            .trained()
277            .unwrap_or_else(|| Arc::new(crate::segment::TrainedVectorStructures::default()));
278
279        let new_reader = Searcher::from_snapshot_reuse(
280            self.segment_manager.directory(),
281            Arc::clone(&self.schema),
282            snapshot,
283            trained,
284            self.resources.clone(),
285            &existing_segments,
286        )
287        .await?;
288
289        // Atomic swap — readers see old or new state, never a torn read
290        self.state.store(Arc::new(SearcherState {
291            searcher: Arc::new(new_reader),
292            segment_ids: new_segment_ids,
293        }));
294
295        Ok(())
296    }
297
298    /// Get schema
299    pub fn schema(&self) -> &Schema {
300        &self.schema
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn reload_guard_releases_flag_on_unwind() {
310        let reloading = AtomicBool::new(true);
311        let result = std::panic::catch_unwind(|| {
312            let _guard = ReloadGuard(&reloading);
313            panic!("cancel reload");
314        });
315        assert!(result.is_err());
316        assert!(!reloading.load(Ordering::Acquire));
317    }
318}