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 trained centroids from ArcSwap (lock-free)
147        let trained = segment_manager.trained();
148        let trained_centroids = trained
149            .as_ref()
150            .map(|t| t.centroids.clone())
151            .unwrap_or_default();
152
153        Searcher::from_snapshot(
154            segment_manager.directory(),
155            Arc::clone(schema),
156            snapshot,
157            trained_centroids,
158            resources,
159        )
160        .await
161    }
162
163    /// Set reload check interval
164    pub fn set_reload_interval(&mut self, interval: std::time::Duration) {
165        self.reload_check_interval = interval;
166    }
167
168    /// Get current searcher (reloads only if segments changed)
169    ///
170    /// Wait-free read path via ArcSwap::load(). Reload checks are guarded
171    /// by an AtomicBool to prevent concurrent reloads.
172    pub async fn searcher(&self) -> Result<Arc<Searcher<D>>> {
173        // Check if we should check for segment changes
174        let should_check = {
175            let last = self.last_reload_check.read();
176            last.elapsed() >= self.reload_check_interval
177        };
178
179        if should_check {
180            // Try to acquire the reload guard (non-blocking)
181            if self
182                .reloading
183                .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
184                .is_ok()
185            {
186                let _reload_guard = ReloadGuard(&self.reloading);
187                // We won the race — do the reload check
188                self.do_reload_check().await?;
189            }
190            // Otherwise another reload is in progress — just return current searcher
191        }
192
193        // Wait-free load (no lock contention with reloads)
194        Ok(Arc::clone(&self.state.load().searcher))
195    }
196
197    /// Actual reload check (called under the `reloading` guard)
198    async fn do_reload_check(&self) -> Result<()> {
199        *self.last_reload_check.write() = std::time::Instant::now();
200
201        // Get current segment IDs from segment manager
202        let new_segment_ids = self.segment_manager.get_segment_ids().await;
203
204        // Check if segments actually changed (wait-free read)
205        let segments_changed = {
206            let state = self.state.load();
207            state.segment_ids != new_segment_ids
208        };
209
210        if segments_changed {
211            let old_count = self.state.load().segment_ids.len();
212            let new_count = new_segment_ids.len();
213            log::info!(
214                "[index_reload] old_count={} new_count={}",
215                old_count,
216                new_count
217            );
218            self.reload_with_segments(new_segment_ids).await?;
219        }
220        Ok(())
221    }
222
223    /// Force reload reader with fresh snapshot.
224    ///
225    /// Waits for any in-progress reload (from `searcher()`) to finish, then
226    /// performs its own reload with the latest segment IDs. This guarantees
227    /// the reload actually happens — unlike `searcher()` which silently skips
228    /// if another reload is in progress.
229    pub async fn reload(&self) -> Result<()> {
230        // Wait for any in-progress reload to finish, then acquire the guard.
231        // This is critical: a concurrent do_reload_check() may have started
232        // before a commit, so its reload won't see the new segments.
233        loop {
234            if self
235                .reloading
236                .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
237                .is_ok()
238            {
239                break;
240            }
241            tokio::task::yield_now().await;
242        }
243        let _reload_guard = ReloadGuard(&self.reloading);
244        let new_segment_ids = self.segment_manager.get_segment_ids().await;
245
246        // Fast path: skip reload if segments haven't changed
247        let segments_changed = {
248            let state = self.state.load();
249            state.segment_ids != new_segment_ids
250        };
251
252        if segments_changed {
253            self.reload_with_segments(new_segment_ids).await
254        } else {
255            log::debug!("[reload] segments unchanged, skipping");
256            Ok(())
257        }
258    }
259
260    /// Internal reload with specific segment IDs.
261    /// Reuses existing segment readers for unchanged segments (avoids re-opening
262    /// mmaps, fast fields, sparse indexes, etc.).
263    /// Atomic swap via ArcSwap::store (wait-free for readers).
264    async fn reload_with_segments(&self, new_segment_ids: Vec<String>) -> Result<()> {
265        // Collect existing segment readers for reuse
266        let existing_segments: Vec<Arc<crate::segment::SegmentReader>> =
267            self.state.load().searcher.segment_readers().to_vec();
268
269        // Acquire the snapshot BEFORE reading trained centroids: producers
270        // capture trained structures only after the ArcSwap publication, so
271        // any ANN segment visible in the snapshot is always satisfiable by a
272        // trained value loaded after the snapshot (see create_reader).
273        let snapshot = self.segment_manager.acquire_snapshot().await;
274
275        // Read trained centroids from ArcSwap (lock-free)
276        let trained = self.segment_manager.trained();
277        let trained_centroids = trained
278            .as_ref()
279            .map(|t| t.centroids.clone())
280            .unwrap_or_default();
281
282        let new_reader = Searcher::from_snapshot_reuse(
283            self.segment_manager.directory(),
284            Arc::clone(&self.schema),
285            snapshot,
286            trained_centroids,
287            self.resources.clone(),
288            &existing_segments,
289        )
290        .await?;
291
292        // Atomic swap — readers see old or new state, never a torn read
293        self.state.store(Arc::new(SearcherState {
294            searcher: Arc::new(new_reader),
295            segment_ids: new_segment_ids,
296        }));
297
298        Ok(())
299    }
300
301    /// Get schema
302    pub fn schema(&self) -> &Schema {
303        &self.schema
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    #[test]
312    fn reload_guard_releases_flag_on_unwind() {
313        let reloading = AtomicBool::new(true);
314        let result = std::panic::catch_unwind(|| {
315            let _guard = ReloadGuard(&reloading);
316            panic!("cancel reload");
317        });
318        assert!(result.is_err());
319        assert!(!reloading.load(Ordering::Acquire));
320    }
321}