Skip to main content

linkrs_sync/
vector_error.rs

1use thiserror::Error;
2
3#[derive(Error, Debug, Clone)]
4pub enum VectorError {
5    #[error("Vector index not found: {0}")]
6    IndexNotFound(String),
7
8    #[error("Vector index already exists: {0}")]
9    IndexAlreadyExists(String),
10
11    #[error("Vector engine not found for space {space_id}, tag {tag_name}, field {field_name}")]
12    EngineNotFound {
13        space_id: u64,
14        tag_name: String,
15        field_name: String,
16    },
17
18    #[error("Vector engine unavailable: {0}")]
19    EngineUnavailable(String),
20
21    #[error("Vector index corrupted: {0}")]
22    IndexCorrupted(String),
23
24    #[error("Qdrant engine error: {0}")]
25    QdrantError(String),
26
27    #[error("Vector dimension mismatch: expected {expected}, got {actual}")]
28    DimensionMismatch { expected: usize, actual: usize },
29
30    #[error("Invalid vector: {0}")]
31    InvalidVector(String),
32
33    #[error("Invalid point ID: {0}")]
34    InvalidPointId(String),
35
36    #[error("Vector index configuration error: {0}")]
37    ConfigError(String),
38
39    #[error("Vector search timeout")]
40    Timeout,
41
42    #[error("Vector index is locked: {0}")]
43    Locked(String),
44
45    #[error("Vector operation cancelled")]
46    Cancelled,
47
48    #[error("Embedding service error: {0}")]
49    EmbeddingError(String),
50
51    #[error("Connection failed: {0}")]
52    ConnectionFailed(String),
53
54    #[error("Collection not found: {0}")]
55    CollectionNotFound(String),
56
57    #[error("Internal error: {0}")]
58    Internal(String),
59}
60
61#[derive(Error, Debug, Clone)]
62pub enum VectorCoordinatorError {
63    #[error("Vector index error: {0}")]
64    Vector(#[from] VectorError),
65
66    #[error("Sync error: {0}")]
67    Sync(String),
68
69    #[error("Vector index creation failed for {tag_name}.{field_name}: {reason}")]
70    IndexCreationFailed {
71        tag_name: String,
72        field_name: String,
73        reason: String,
74    },
75
76    #[error("Vector index drop failed for {tag_name}.{field_name}: {reason}")]
77    IndexDropFailed {
78        tag_name: String,
79        field_name: String,
80        reason: String,
81    },
82
83    #[error("Vector index rebuild failed: {0}")]
84    IndexRebuildFailed(String),
85
86    #[error("Vector rebuild busy: {0}")]
87    RebuildBusy(String),
88
89    #[error("Vertex change processing failed: {0}")]
90    VertexChangeFailed(String),
91
92    #[error("Space not found: {0}")]
93    SpaceNotFound(u64),
94
95    #[error("Tag not found: {0}")]
96    TagNotFound(String),
97
98    #[error("Field not vector indexed: {tag_name}.{field_name}")]
99    FieldNotIndexed {
100        tag_name: String,
101        field_name: String,
102    },
103
104    #[error("Vector coordinator not initialized")]
105    NotInitialized,
106
107    #[error("Vector coordinator is shutting down")]
108    ShuttingDown,
109
110    #[error(
111        "Vector engine is disabled by configuration; \
112         enable the vector engine (or check VectorEngineState) before issuing vector operations"
113    )]
114    EngineDisabled,
115
116    #[error("Invalid operation: {0}")]
117    InvalidOperation(String),
118
119    #[error("Embedding service not available")]
120    EmbeddingServiceNotAvailable,
121
122    #[error("Embedding error: {0}")]
123    EmbeddingError(String),
124
125    #[error("Collection config conflict for {collection_name}: existing {existing_size}/{existing_dist}, requested {requested_size}/{requested_dist}")]
126    CollectionConfigConflict {
127        collection_name: String,
128        existing_size: usize,
129        existing_dist: String,
130        requested_size: usize,
131        requested_dist: String,
132    },
133
134    #[error("Internal error: {0}")]
135    Internal(String),
136
137    #[error("Buffer error: {0}")]
138    BufferError(String),
139}
140
141/// Classification of vector backend errors for retry policy.
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum VectorErrorKind {
144    /// Transient failure that should be retried with exponential backoff.
145    Retryable,
146    /// Permanent failure that will never succeed (e.g. dimension mismatch).
147    NonRetryable,
148    /// Authentication / authorization failure that should pause delivery.
149    Auth,
150}
151
152impl VectorErrorKind {
153    /// Classify a [`VectorError`] into a retry bucket.
154    pub fn classify(err: &VectorError) -> Self {
155        match err {
156            VectorError::DimensionMismatch { .. }
157            | VectorError::InvalidVector(_)
158            | VectorError::InvalidPointId(_)
159            | VectorError::ConfigError(_)
160            | VectorError::IndexAlreadyExists(_)
161            | VectorError::CollectionNotFound(_)
162            | VectorError::IndexNotFound(_) => Self::NonRetryable,
163            VectorError::ConnectionFailed(_)
164            | VectorError::EngineUnavailable(_)
165            | VectorError::Timeout
166            | VectorError::QdrantError(_)
167            | VectorError::IndexCorrupted(_)
168            | VectorError::Locked(_)
169            | VectorError::Internal(_) => Self::Retryable,
170            VectorError::Cancelled
171            | VectorError::EmbeddingError(_)
172            | VectorError::EngineNotFound { .. } => Self::NonRetryable,
173        }
174    }
175
176    /// Classify a stringified error (used in `SyncManager::retry_outbox_sync`
177    /// where the error has been erased to `String`).
178    pub fn classify_str(msg: &str) -> Self {
179        let lower = msg.to_ascii_lowercase();
180        if lower.contains("dimensionmismatch")
181            || lower.contains("dimension mismatch")
182            || lower.contains("invalidconfig")
183            || lower.contains("invalid vector")
184            || lower.contains("invalid point id")
185            || lower.contains("invalidargument")
186            || lower.contains("configerror")
187        {
188            return Self::NonRetryable;
189        }
190        if lower.contains("auth")
191            || lower.contains("unauthorized")
192            || lower.contains("forbidden")
193            || lower.contains("permission")
194            || lower.contains("apikey")
195            || lower.contains("api key")
196        {
197            return Self::Auth;
198        }
199        if lower.contains("engine is disabled") || lower.contains("enginedisabled") {
200            return Self::Retryable;
201        }
202        if lower.contains("timeout")
203            || lower.contains("unavailable")
204            || lower.contains("connection")
205            || lower.contains("transport")
206            || lower.contains("deadline")
207            || lower.contains("temporarily")
208            || lower.contains("rate limit")
209            || lower.contains("qdrant")
210        {
211            return Self::Retryable;
212        }
213        // Default to NonRetryable for unknowns to avoid infinite retries on
214        // validation failures; callers may still retry with capped attempts.
215        // However for safety in outbox path treat unknown as Retryable unless
216        // it clearly looks like a validation failure.
217        if lower.contains("not found") || lower.contains("already exists") {
218            return Self::NonRetryable;
219        }
220        Self::Retryable
221    }
222}
223
224impl VectorError {
225    /// Convenience wrapper for [`VectorErrorKind::classify`].
226    pub fn kind(&self) -> VectorErrorKind {
227        VectorErrorKind::classify(self)
228    }
229}
230
231impl VectorCoordinatorError {
232    /// Classify a coordinator error into a retry bucket.
233    pub fn kind(&self) -> VectorErrorKind {
234        match self {
235            VectorCoordinatorError::Vector(err) => VectorErrorKind::classify(err),
236            VectorCoordinatorError::EngineDisabled => VectorErrorKind::Retryable,
237            VectorCoordinatorError::CollectionConfigConflict { .. }
238            | VectorCoordinatorError::FieldNotIndexed { .. }
239            | VectorCoordinatorError::InvalidOperation(_)
240            | VectorCoordinatorError::EmbeddingServiceNotAvailable
241            | VectorCoordinatorError::EmbeddingError(_)
242            | VectorCoordinatorError::NotInitialized
243            | VectorCoordinatorError::ShuttingDown
244            | VectorCoordinatorError::BufferError(_) => VectorErrorKind::NonRetryable,
245            VectorCoordinatorError::IndexCreationFailed { .. }
246            | VectorCoordinatorError::IndexDropFailed { .. }
247            | VectorCoordinatorError::IndexRebuildFailed(_)
248            | VectorCoordinatorError::RebuildBusy(_)
249            | VectorCoordinatorError::VertexChangeFailed(_)
250            | VectorCoordinatorError::SpaceNotFound(_)
251            | VectorCoordinatorError::TagNotFound(_)
252            | VectorCoordinatorError::Sync(_)
253            | VectorCoordinatorError::Internal(_) => VectorErrorKind::Retryable,
254        }
255    }
256
257    /// Classify from the display string of the error.
258    pub fn kind_str(msg: &str) -> VectorErrorKind {
259        VectorErrorKind::classify_str(msg)
260    }
261}
262
263pub type VectorResult<T> = std::result::Result<T, VectorError>;
264pub type VectorCoordinatorResult<T> = std::result::Result<T, VectorCoordinatorError>;
265
266#[cfg(feature = "vector")]
267impl From<simvec::VectorSearchError> for VectorError {
268    fn from(err: simvec::VectorSearchError) -> Self {
269        match err {
270            simvec::VectorSearchError::CollectionNotFound(name) => VectorError::IndexNotFound(name),
271            simvec::VectorSearchError::CollectionAlreadyExists(name) => {
272                VectorError::IndexAlreadyExists(name)
273            }
274            simvec::VectorSearchError::CollectionIncomplete { dir, file } => {
275                VectorError::IndexCorrupted(format!(
276                    "collection incomplete (missing {}): {}",
277                    file,
278                    dir.display()
279                ))
280            }
281            simvec::VectorSearchError::InvalidCollectionName(name) => {
282                VectorError::ConfigError(format!("Invalid collection name: {}", name))
283            }
284            simvec::VectorSearchError::InvalidConfig(msg) => VectorError::ConfigError(msg),
285            simvec::VectorSearchError::InsufficientSpace {
286                operation,
287                needed,
288                available,
289            } => VectorError::ConfigError(format!(
290                "insufficient space for {}: needs {} bytes, {} available",
291                operation, needed, available
292            )),
293            simvec::VectorSearchError::CapacityExceeded {
294                collection,
295                max_points,
296                requested,
297            } => VectorError::ConfigError(format!(
298                "collection {} capacity exceeded: limit {}, requested total {}",
299                collection, max_points, requested
300            )),
301            simvec::VectorSearchError::InvalidVectorDimension { expected, actual } => {
302                VectorError::DimensionMismatch { expected, actual }
303            }
304            simvec::VectorSearchError::InvalidPointId(id) => VectorError::InvalidPointId(id),
305            simvec::VectorSearchError::NonFiniteElement(index) => {
306                VectorError::InvalidVector(format!("non-finite element at index {}", index))
307            }
308            simvec::VectorSearchError::UnsupportedMetric(metric) => {
309                VectorError::ConfigError(format!("metric not supported: {:?}", metric))
310            }
311            simvec::VectorSearchError::Filter(msg) => VectorError::InvalidVector(msg),
312            simvec::VectorSearchError::CorruptData(msg) => VectorError::IndexCorrupted(msg),
313            simvec::VectorSearchError::Io(e) => VectorError::Internal(e.to_string()),
314            simvec::VectorSearchError::Serialization(e) => VectorError::Internal(e.to_string()),
315            simvec::VectorSearchError::Json(e) => VectorError::Internal(e.to_string()),
316            simvec::VectorSearchError::Internal(msg) => VectorError::Internal(msg),
317        }
318    }
319}
320
321#[cfg(feature = "vector")]
322impl From<simvec::VectorSearchError> for VectorCoordinatorError {
323    fn from(err: simvec::VectorSearchError) -> Self {
324        VectorCoordinatorError::Vector(VectorError::from(err))
325    }
326}
327
328#[cfg(feature = "vector")]
329impl From<simvec::VectorEngineError> for VectorCoordinatorError {
330    fn from(err: simvec::VectorEngineError) -> Self {
331        match err {
332            simvec::VectorEngineError::Local(msg) => {
333                VectorCoordinatorError::Vector(VectorError::Internal(msg))
334            }
335            simvec::VectorEngineError::Remote(msg) => {
336                VectorCoordinatorError::Vector(VectorError::QdrantError(msg))
337            }
338            simvec::VectorEngineError::Internal(msg) => {
339                VectorCoordinatorError::Vector(VectorError::Internal(msg))
340            }
341            simvec::VectorEngineError::NotSupported(op) => VectorCoordinatorError::Vector(
342                VectorError::ConfigError(format!("Operation not supported: {}", op)),
343            ),
344        }
345    }
346}
347
348#[cfg(feature = "vector-qdrant")]
349impl From<vector_client::VectorClientError> for VectorError {
350    fn from(err: vector_client::VectorClientError) -> Self {
351        match err {
352            vector_client::VectorClientError::ConnectionFailed(msg) => {
353                VectorError::ConnectionFailed(msg)
354            }
355            vector_client::VectorClientError::CollectionNotFound(name) => {
356                VectorError::CollectionNotFound(name)
357            }
358            vector_client::VectorClientError::CollectionAlreadyExists(name) => {
359                VectorError::IndexAlreadyExists(name)
360            }
361            vector_client::VectorClientError::PointNotFound(id, _collection) => {
362                VectorError::IndexNotFound(id)
363            }
364            vector_client::VectorClientError::InvalidVectorDimension { expected, actual } => {
365                VectorError::DimensionMismatch { expected, actual }
366            }
367            vector_client::VectorClientError::InvalidCollectionName(name) => {
368                VectorError::ConfigError(format!("Invalid collection name: {}", name))
369            }
370            vector_client::VectorClientError::InvalidPointId(id) => VectorError::InvalidPointId(id),
371            vector_client::VectorClientError::Timeout(_msg) => VectorError::Timeout,
372            vector_client::VectorClientError::InvalidConfig(msg) => VectorError::ConfigError(msg),
373            vector_client::VectorClientError::SearchError(msg) => VectorError::QdrantError(msg),
374            vector_client::VectorClientError::UpsertError(msg) => VectorError::QdrantError(msg),
375            vector_client::VectorClientError::DeleteError(msg) => VectorError::QdrantError(msg),
376            vector_client::VectorClientError::PayloadError(msg) => VectorError::QdrantError(msg),
377            vector_client::VectorClientError::FilterError(msg) => VectorError::InvalidVector(msg),
378            vector_client::VectorClientError::HealthCheckFailed(msg) => {
379                VectorError::ConnectionFailed(msg)
380            }
381            vector_client::VectorClientError::EngineNotInitialized => {
382                VectorError::EngineUnavailable("Engine not initialized".to_string())
383            }
384            vector_client::VectorClientError::EngineNotAvailable(name) => {
385                VectorError::EngineUnavailable(format!("Engine {} not available", name))
386            }
387            vector_client::VectorClientError::IndexAlreadyExists(name) => {
388                VectorError::IndexAlreadyExists(name)
389            }
390            vector_client::VectorClientError::IoError(e) => VectorError::Internal(e.to_string()),
391            vector_client::VectorClientError::SerializationError(e) => {
392                VectorError::Internal(e.to_string())
393            }
394            vector_client::VectorClientError::InternalError(msg) => VectorError::Internal(msg),
395            vector_client::VectorClientError::QdrantHttpError { status, message } => {
396                VectorError::QdrantError(format!("HTTP {}: {}", status, message))
397            }
398            vector_client::VectorClientError::QdrantGrpcError(msg) => VectorError::QdrantError(msg),
399            vector_client::VectorClientError::NotSupported(op) => {
400                VectorError::ConfigError(format!("Operation not supported: {}", op))
401            }
402        }
403    }
404}
405
406#[cfg(feature = "vector-qdrant")]
407impl From<vector_client::VectorClientError> for VectorCoordinatorError {
408    fn from(err: vector_client::VectorClientError) -> Self {
409        VectorCoordinatorError::Vector(VectorError::from(err))
410    }
411}