Skip to main content

linkrs_sync/manager/
accessors.rs

1//! Read-only accessors, rebuild status and index facades.
2use super::*;
3#[cfg(feature = "fulltext")]
4use crate::coordinator::SyncCoordinator;
5#[cfg(any(feature = "fulltext", feature = "vector"))]
6use crate::sqlite_outbox::SqliteOutbox;
7#[cfg(feature = "vector")]
8use crate::vector_sync::VectorSyncCoordinator;
9#[cfg(any(feature = "fulltext", feature = "vector"))]
10use linkrs_metrics::StatsManager;
11impl super::SyncManager {
12    /// Admission lock for one index rebuild. The winner holds the guard for
13    /// the whole rebuild; concurrent attempts fail fast with
14    /// `SyncError::RebuildBusy` instead of interleaving generations on the
15    /// same outbox `tag_index_id`.
16    #[cfg(any(feature = "fulltext", feature = "vector"))]
17    pub(crate) fn rebuild_lock_for(
18        &self,
19        target: &str,
20        space_id: u64,
21        tag_name: &str,
22        field_name: &str,
23    ) -> Arc<tokio::sync::Mutex<()>> {
24        let key = format!("{target}:{space_id}:{tag_name}:{field_name}");
25        self.rebuild_locks
26            .entry(key)
27            .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(())))
28            .clone()
29    }
30
31    #[cfg(feature = "fulltext")]
32    pub fn sync_coordinator(&self) -> &Arc<SyncCoordinator> {
33        self.sync_coordinator
34            .as_ref()
35            .expect("SyncCoordinator not available without fulltext feature")
36    }
37
38    #[cfg(feature = "fulltext")]
39    pub(crate) fn sync_coordinator_opt(&self) -> Option<&Arc<SyncCoordinator>> {
40        self.sync_coordinator.as_ref()
41    }
42
43    #[cfg(any(feature = "fulltext", feature = "vector"))]
44    pub(crate) fn sqlite_outbox_opt(&self) -> Option<&SqliteOutbox> {
45        self.sqlite_outbox.as_deref()
46    }
47
48    #[cfg(feature = "vector")]
49    pub fn vector_coordinator(&self) -> Option<&Arc<VectorSyncCoordinator>> {
50        self.vector_coordinator.as_ref()
51    }
52
53    #[cfg(feature = "fulltext")]
54    pub fn fulltext_manager(&self) -> Arc<linkrs_fulltext::manager::FulltextIndexManager> {
55        self.sync_coordinator
56            .as_ref()
57            .expect("SyncCoordinator not available without fulltext feature")
58            .fulltext_manager()
59            .clone()
60    }
61
62    /// Non-panicking fulltext manager access for read-only diagnostics and
63    /// maintenance endpoints. `None` when the fulltext target is not
64    /// configured.
65    #[cfg(feature = "fulltext")]
66    pub fn fulltext_manager_opt(
67        &self,
68    ) -> Option<Arc<linkrs_fulltext::manager::FulltextIndexManager>> {
69        self.sync_coordinator
70            .as_ref()
71            .map(|coordinator| coordinator.fulltext_manager().clone())
72    }
73
74    #[cfg(any(feature = "fulltext", feature = "vector"))]
75    pub(crate) fn stats_manager_opt(&self) -> Option<Arc<StatsManager>> {
76        self.stats_manager.clone()
77    }
78
79    /// Fulltext indexes whose engine is in the `Inconsistent` state. These
80    /// reject writes and need operator attention (online rebuild or drop and
81    /// recreate). Empty when fulltext is not configured.
82    #[cfg(feature = "fulltext")]
83    pub fn inconsistent_fulltext_indexes(&self) -> Vec<linkrs_fulltext::IndexMetadata> {
84        self.fulltext_manager_opt()
85            .map(|manager| manager.get_inconsistent_indexes())
86            .unwrap_or_default()
87    }
88
89    /// Vector rebuilds stuck in the `Failed` phase. Live data still serves;
90    /// retry the rebuild. Mirrors the fulltext inconsistent listing so both
91    /// engines share one alerting shape. Empty when vector is not configured.
92    #[cfg(feature = "vector")]
93    pub fn inconsistent_vector_indexes(&self) -> Vec<linkrs_core::index_events::RebuildProgress> {
94        self.vector_coordinator
95            .as_ref()
96            .map(|coordinator| coordinator.inconsistent_vector_indexes())
97            .unwrap_or_default()
98    }
99
100    /// True when any configured engine reports an index needing attention.
101    #[cfg(any(feature = "fulltext", feature = "vector"))]
102    pub fn has_inconsistent_indexes(&self) -> bool {
103        #[cfg(feature = "fulltext")]
104        if !self.inconsistent_fulltext_indexes().is_empty() {
105            return true;
106        }
107        #[cfg(feature = "vector")]
108        if !self.inconsistent_vector_indexes().is_empty() {
109            return true;
110        }
111        false
112    }
113
114    /// Unified rebuild progress for one logical index.
115    ///
116    /// Fulltext and vector rebuilds share the same `RebuildProgress` shape but
117    /// store it in different managers. This helper checks the fulltext manager
118    /// first and then the vector coordinator, so operators poll a single
119    /// endpoint instead of knowing which engine backs `(space, tag, field)`.
120    /// Returns `None` when neither target has ever run a rebuild for the key.
121    #[cfg(any(feature = "fulltext", feature = "vector"))]
122    pub fn rebuild_progress(
123        &self,
124        space_id: u64,
125        tag_name: &str,
126        field_name: &str,
127    ) -> Option<linkrs_core::index_events::RebuildProgress> {
128        #[cfg(feature = "fulltext")]
129        if let Some(manager) = self.fulltext_manager_opt() {
130            if let Some(progress) = manager.rebuild_progress(space_id, tag_name, field_name) {
131                return Some(progress);
132            }
133        }
134        #[cfg(feature = "vector")]
135        if let Some(coordinator) = self.vector_coordinator.as_ref() {
136            if let Some(progress) = coordinator.rebuild_progress(space_id, tag_name, field_name) {
137                return Some(progress);
138            }
139        }
140        None
141    }
142
143    /// No rebuild engines are compiled in, so no progress can exist.
144    #[cfg(not(any(feature = "fulltext", feature = "vector")))]
145    pub fn rebuild_progress(
146        &self,
147        _space_id: u64,
148        _tag_name: &str,
149        _field_name: &str,
150    ) -> Option<linkrs_core::index_events::RebuildProgress> {
151        None
152    }
153
154    /// Fail non-terminal rebuild generations stranded by crashed attempts for
155    /// every configured target, and collect fulltext rebuild scratch state.
156    /// Idempotent; safe to run at startup before serving traffic. Missing
157    /// targets count as zero instead of failing startup.
158    pub async fn recover_stale_rebuilds(&self) -> Result<usize, SyncError> {
159        let fulltext_failed = self.recover_stale_fulltext_rebuilds_tolerant().await?;
160        let vector_failed = self.recover_stale_vector_rebuilds_tolerant().await?;
161        Ok(fulltext_failed + vector_failed)
162    }
163
164    #[cfg(feature = "fulltext")]
165    async fn recover_stale_fulltext_rebuilds_tolerant(&self) -> Result<usize, SyncError> {
166        match self.recover_stale_fulltext_rebuilds().await {
167            Ok(count) => Ok(count),
168            Err(SyncError::PersistenceError(_)) => Ok(0),
169            Err(error) => Err(error),
170        }
171    }
172
173    #[cfg(not(feature = "fulltext"))]
174    async fn recover_stale_fulltext_rebuilds_tolerant(&self) -> Result<usize, SyncError> {
175        Ok(0)
176    }
177
178    #[cfg(feature = "vector")]
179    async fn recover_stale_vector_rebuilds_tolerant(&self) -> Result<usize, SyncError> {
180        match self.recover_stale_vector_rebuilds().await {
181            Ok(count) => Ok(count),
182            Err(SyncError::PersistenceError(_)) => Ok(0),
183            Err(error) => Err(error),
184        }
185    }
186
187    #[cfg(not(feature = "vector"))]
188    async fn recover_stale_vector_rebuilds_tolerant(&self) -> Result<usize, SyncError> {
189        Ok(0)
190    }
191
192    /// Sync wrapper around [`SyncManager::recover_stale_rebuilds`] for
193    /// startup paths without an async context.
194    pub fn recover_stale_rebuilds_sync(&self) -> Result<usize, SyncError> {
195        self.execute_sync(|| async { self.recover_stale_rebuilds().await })
196    }
197
198    pub fn get_dead_letter_entries(&self) -> Vec<crate::DeadLetterEntry> {
199        if let Some(ref dlq) = self.dead_letter_queue {
200            dlq.get_all()
201        } else {
202            vec![]
203        }
204    }
205
206    pub fn get_unrecovered_entries(&self) -> Vec<crate::DeadLetterEntry> {
207        if let Some(ref dlq) = self.dead_letter_queue {
208            dlq.get_unrecovered()
209        } else {
210            vec![]
211        }
212    }
213
214    pub fn get_old_dead_letter_entries(
215        &self,
216        age: std::time::Duration,
217    ) -> Vec<crate::DeadLetterEntry> {
218        if let Some(ref dlq) = self.dead_letter_queue {
219            dlq.get_old_entries(age)
220        } else {
221            vec![]
222        }
223    }
224
225    pub fn remove_dead_letter_entry(&self, index: usize) -> Option<crate::DeadLetterEntry> {
226        if let Some(ref dlq) = self.dead_letter_queue {
227            dlq.remove(index)
228        } else {
229            None
230        }
231    }
232
233    pub fn get_dlq_size(&self) -> usize {
234        if let Some(ref dlq) = self.dead_letter_queue {
235            dlq.get_all().len()
236        } else {
237            0
238        }
239    }
240
241    pub fn get_unrecovered_dlq_size(&self) -> usize {
242        if let Some(ref dlq) = self.dead_letter_queue {
243            dlq.get_unrecovered().len()
244        } else {
245            0
246        }
247    }
248
249    #[cfg(feature = "vector")]
250    pub fn vector_index_exists(&self, space_id: u64, tag_name: &str, field_name: &str) -> bool {
251        if let Some(ref vector_coord) = self.vector_coordinator {
252            vector_coord.index_exists(space_id, tag_name, field_name)
253        } else {
254            false
255        }
256    }
257
258    #[cfg(feature = "vector")]
259    pub async fn create_vector_index(
260        &self,
261        space_id: u64,
262        tag_name: &str,
263        field_name: &str,
264        vector_size: usize,
265        distance: simvec::DistanceMetric,
266    ) -> Result<String, SyncError> {
267        if let Some(ref vector_coord) = self.vector_coordinator {
268            vector_coord
269                .create_vector_index(space_id, tag_name, field_name, vector_size, distance)
270                .await
271                .map_err(|e| SyncError::VectorError(e.to_string()))
272        } else {
273            Err(SyncError::Internal(
274                "Vector coordinator not available".to_string(),
275            ))
276        }
277    }
278
279    #[cfg(feature = "vector")]
280    pub async fn drop_vector_index(
281        &self,
282        space_id: u64,
283        tag_name: &str,
284        field_name: &str,
285    ) -> Result<(), SyncError> {
286        if let Some(ref vector_coord) = self.vector_coordinator {
287            vector_coord
288                .drop_vector_index(space_id, tag_name, field_name)
289                .await
290                .map_err(|e| SyncError::VectorError(e.to_string()))
291        } else {
292            Err(SyncError::Internal(
293                "Vector coordinator not available".to_string(),
294            ))
295        }
296    }
297
298    #[cfg(feature = "vector")]
299    pub async fn search_vector(
300        &self,
301        space_id: u64,
302        tag_name: &str,
303        field_name: &str,
304        vector: &[f32],
305        top_k: usize,
306    ) -> Result<Vec<SearchResult>, SyncError> {
307        if let Some(ref vector_coord) = self.vector_coordinator {
308            let options = crate::vector_sync::SearchOptions::new(
309                space_id,
310                tag_name,
311                field_name,
312                vector.to_vec(),
313                top_k,
314            );
315            vector_coord
316                .search_with_options(options)
317                .await
318                .map_err(|e| SyncError::VectorError(e.to_string()))
319        } else {
320            Err(SyncError::Internal(
321                "Vector coordinator not available".to_string(),
322            ))
323        }
324    }
325}