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