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