qdrant_edge/segment/common/
operation_error.rs1use std::backtrace::Backtrace;
2use std::collections::TryReserveError;
3use std::io::{Error as IoError, ErrorKind};
4use std::path::PathBuf;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::time::Duration;
7
8use atomicwrites::Error as AtomicIoError;
9use crate::blobstore::error::BlobstoreError;
10use crate::common::bitpacking_ordered::DecompressionError;
11use crate::common::mmap::Error as MmapError;
12use crate::common::universal_io::{IsNotFound, UniversalIoError};
13use rayon::ThreadPoolBuildError;
14use thiserror::Error;
15
16use crate::segment::types::{PayloadKeyType, PointIdType, SeqNumberType, VectorNameBuf};
17use crate::segment::utils::mem::Mem;
18
19pub const PROCESS_CANCELLED_BY_SERVICE_MESSAGE: &str = "process cancelled by service";
20
21#[derive(Error, Debug, Clone, PartialEq)]
22#[error("{0}")]
23pub enum OperationError {
24 #[error("Vector dimension error: expected dim: {expected_dim}, got {received_dim}")]
25 WrongVectorDimension {
26 expected_dim: usize,
27 received_dim: usize,
28 },
29 #[error("{description}")]
35 MalformedVectorBlob { description: String },
36 #[error("Not existing vector name error: {received_name}")]
37 VectorNameNotExists { received_name: VectorNameBuf },
38 #[error("No point with id {missed_point_id}")]
39 PointIdError { missed_point_id: PointIdType },
40 #[error(
41 "Payload type does not match with previously given for field {field_name}. Expected: {expected_type}"
42 )]
43 TypeError {
44 field_name: PayloadKeyType,
45 expected_type: String,
46 },
47 #[error("Unable to infer type for the field '{field_name}'. Please specify `field_type`")]
48 TypeInferenceError { field_name: PayloadKeyType },
49 #[error("Service runtime error: {description}")]
52 ServiceError {
53 description: String,
54 backtrace: Option<String>,
55 },
56 #[error("Inconsistent storage: {description}")]
57 InconsistentStorage { description: String },
58 #[error("Storage file not found: {}", path.display())]
62 FileNotFound { path: PathBuf },
63 #[error("Out of memory, free: {free}, {description}")]
64 OutOfMemory { description: String, free: u64 },
65 #[error("Operation cancelled: {description}")]
66 Cancelled { description: String },
67 #[error("Timeout error: {description}")]
68 Timeout { description: String },
69 #[error("Validation failed: {description}")]
70 ValidationError { description: String },
71 #[error("Wrong usage of sparse vectors")]
72 WrongSparse,
73 #[error("Wrong usage of multi vectors")]
74 WrongMulti,
75 #[error(
76 "No range index for `order_by` key: `{key}`. Please create one to use `order_by`. Check https://qdrant.tech/documentation/concepts/indexing/#payload-index to see which payload schemas support Range conditions"
77 )]
78 MissingRangeIndexForOrderBy { key: String },
79 #[error(
80 "No appropriate index for faceting: `{key}`. Please create one to facet on this field. Check https://qdrant.tech/documentation/concepts/indexing/#payload-index to see which payload schemas support Match conditions"
81 )]
82 MissingMapIndexForFacet { key: String },
83 #[error(
84 "Expected {expected_type} value for {field_name} in the payload and/or in the formula defaults. Error: {description}"
85 )]
86 VariableTypeError {
87 field_name: PayloadKeyType,
88 expected_type: String,
89 description: String,
90 },
91 #[error("The expression {expression} produced a non-finite number")]
92 NonFiniteNumber { expression: String },
93 #[error(
97 "All appendable segments reached the maximum segment size of {max_segment_size_bytes} bytes"
98 )]
99 OutOfAppendableCapacity { max_segment_size_bytes: usize },
100}
101
102impl OperationError {
103 pub fn service_error(description: impl Into<String>) -> Self {
106 Self::ServiceError {
107 description: description.into(),
108 backtrace: Some(Backtrace::force_capture().to_string()),
109 }
110 }
111
112 pub fn service_error_light(description: impl Into<String>) -> Self {
114 Self::ServiceError {
115 description: description.into(),
116 backtrace: None,
117 }
118 }
119
120 pub fn validation_error(description: impl Into<String>) -> Self {
121 Self::ValidationError {
122 description: description.into(),
123 }
124 }
125
126 pub fn inconsistent_storage(description: impl Into<String>) -> Self {
127 Self::InconsistentStorage {
128 description: description.into(),
129 }
130 }
131
132 pub fn cancelled(description: impl Into<String>) -> Self {
133 Self::Cancelled {
134 description: description.into(),
135 }
136 }
137
138 pub fn vector_name_not_exists(vector_name: impl Into<String>) -> Self {
139 Self::VectorNameNotExists {
140 received_name: vector_name.into(),
141 }
142 }
143
144 pub fn malformed_vector_blob(description: impl Into<String>) -> Self {
145 Self::MalformedVectorBlob {
146 description: description.into(),
147 }
148 }
149
150 pub fn timeout(timeout: Duration, operation: impl Into<String>) -> Self {
151 Self::Timeout {
152 description: format!(
153 "Operation '{}' timed out after {timeout:?}",
154 operation.into(),
155 ),
156 }
157 }
158}
159
160impl IsNotFound for OperationError {
165 fn is_not_found(&self) -> bool {
166 match self {
167 Self::FileNotFound { .. } => true,
168 Self::WrongVectorDimension { .. }
169 | Self::MalformedVectorBlob { .. }
170 | Self::VectorNameNotExists { .. }
171 | Self::PointIdError { .. }
172 | Self::TypeError { .. }
173 | Self::TypeInferenceError { .. }
174 | Self::ServiceError { .. }
175 | Self::InconsistentStorage { .. }
176 | Self::OutOfMemory { .. }
177 | Self::Cancelled { .. }
178 | Self::Timeout { .. }
179 | Self::ValidationError { .. }
180 | Self::WrongSparse
181 | Self::WrongMulti
182 | Self::MissingRangeIndexForOrderBy { .. }
183 | Self::MissingMapIndexForFacet { .. }
184 | Self::VariableTypeError { .. }
185 | Self::NonFiniteNumber { .. }
186 | Self::OutOfAppendableCapacity { .. } => false,
187 }
188 }
189}
190
191#[derive(Debug, Clone)]
193pub struct SegmentFailedState {
194 pub version: SeqNumberType,
195 pub point_id: Option<PointIdType>,
196 pub error: OperationError,
197}
198
199impl From<ThreadPoolBuildError> for OperationError {
200 fn from(error: ThreadPoolBuildError) -> Self {
201 Self::service_error(error.to_string())
202 }
203}
204
205impl From<DecompressionError> for OperationError {
206 fn from(err: DecompressionError) -> Self {
207 Self::service_error(err.to_string())
208 }
209}
210
211impl From<MmapError> for OperationError {
212 fn from(err: MmapError) -> Self {
213 match err {
214 MmapError::MissingFile(path) => Self::FileNotFound { path: path.into() },
217 err @ (MmapError::SizeExact(..)
218 | MmapError::SizeLess(..)
219 | MmapError::SizeMultiple(..)
220 | MmapError::Io(_)) => Self::service_error(err.to_string()),
221 }
222 }
223}
224
225impl From<UniversalIoError> for OperationError {
226 fn from(err: UniversalIoError) -> Self {
227 match err {
228 UniversalIoError::Io(err) => Self::from(err),
229 UniversalIoError::Mmap(err) => Self::from(err),
230
231 UniversalIoError::NotFound { path } => Self::FileNotFound { path },
232
233 UniversalIoError::Bincode(_)
234 | UniversalIoError::BytemuckCast(_)
235 | UniversalIoError::ZerocopySize(_)
236 | UniversalIoError::IoUringNotSupported(_)
237 | UniversalIoError::OutOfBounds { .. }
238 | UniversalIoError::InvalidFileIndex { .. }
239 | UniversalIoError::Uninitialized { .. }
240 | UniversalIoError::QueueIsFull
241 | UniversalIoError::AppendOffsetConflict { .. }
242 | UniversalIoError::S3(_)
243 | UniversalIoError::S3Config { .. }
244 | UniversalIoError::TaskPanicked(_) => Self::service_error(err.to_string()),
245 }
246 }
247}
248
249impl<Src, Dst: ?Sized> From<zerocopy::SizeError<Src, Dst>> for OperationError
250where
251 zerocopy::SizeError<Src, Dst>: std::fmt::Display,
252{
253 fn from(err: zerocopy::SizeError<Src, Dst>) -> Self {
254 Self::service_error(format!("Zerocopy size error: {err}"))
255 }
256}
257
258impl<A, S, V> From<zerocopy::ConvertError<A, S, V>> for OperationError
259where
260 zerocopy::ConvertError<A, S, V>: std::fmt::Display,
261{
262 fn from(err: zerocopy::ConvertError<A, S, V>) -> Self {
263 Self::service_error(format!("Zerocopy convert error: {err}"))
264 }
265}
266
267impl From<serde_cbor::Error> for OperationError {
268 fn from(err: serde_cbor::Error) -> Self {
269 Self::service_error(format!("Failed to parse data: {err}"))
270 }
271}
272
273impl<E> From<AtomicIoError<E>> for OperationError {
274 fn from(err: AtomicIoError<E>) -> Self {
275 match err {
276 AtomicIoError::Internal(io_err) => Self::from(io_err),
277 AtomicIoError::User(_user_err) => Self::service_error("Unknown atomic write error"),
278 }
279 }
280}
281
282impl From<IoError> for OperationError {
283 fn from(err: IoError) -> Self {
284 #[expect(clippy::wildcard_enum_match_arm, reason = "error handling")]
285 match err.kind() {
286 ErrorKind::OutOfMemory => {
287 let free_memory = Mem::new().available_memory_bytes();
288 Self::OutOfMemory {
289 description: format!("IO Error: {err}"),
290 free: free_memory,
291 }
292 }
293 _ => Self::service_error(format!("IO Error: {err}")),
294 }
295 }
296}
297
298impl From<serde_json::Error> for OperationError {
299 fn from(err: serde_json::Error) -> Self {
300 Self::service_error(format!("Json error: {err}"))
301 }
302}
303
304impl From<fs_extra::error::Error> for OperationError {
305 fn from(err: fs_extra::error::Error) -> Self {
306 Self::service_error(format!("File system error: {err}"))
307 }
308}
309
310impl From<geohash::GeohashError> for OperationError {
311 fn from(err: geohash::GeohashError) -> Self {
312 Self::service_error(format!("Geohash error: {err}"))
313 }
314}
315
316impl From<crate::quantization::EncodingError> for OperationError {
317 fn from(err: crate::quantization::EncodingError) -> Self {
318 match err {
319 crate::quantization::EncodingError::IOError(err)
320 | crate::quantization::EncodingError::EncodingError(err)
321 | crate::quantization::EncodingError::ArgumentsError(err) => {
322 Self::service_error(format!("Quantization encoding error: {err}"))
323 }
324 crate::quantization::EncodingError::Stopped => {
325 Self::cancelled(PROCESS_CANCELLED_BY_SERVICE_MESSAGE)
326 }
327 }
328 }
329}
330
331impl From<TryReserveError> for OperationError {
332 fn from(err: TryReserveError) -> Self {
333 let free_memory = Mem::new().available_memory_bytes();
334 Self::OutOfMemory {
335 description: format!("Failed to reserve memory: {err}"),
336 free: free_memory,
337 }
338 }
339}
340
341impl From<BlobstoreError> for OperationError {
342 fn from(err: BlobstoreError) -> Self {
343 match err {
344 BlobstoreError::ServiceError { description } => {
345 Self::service_error(format!("Blobstore error: {description}"))
346 }
347 BlobstoreError::FlushCancelled => Self::cancelled("Blobstore flushing was cancelled"),
348 BlobstoreError::Io(_) | BlobstoreError::Mmap(_) | BlobstoreError::SerdeJson(_) => {
349 Self::service_error(err.to_string())
350 }
351 BlobstoreError::ValidationError { message } => Self::validation_error(message),
352 BlobstoreError::UnsupportedOperation { .. } => Self::service_error(err.to_string()),
353 BlobstoreError::UniversalIo(err) => match err {
354 UniversalIoError::NotFound { path } => Self::FileNotFound { path },
355 err @ (UniversalIoError::Io(_)
356 | UniversalIoError::Mmap(_)
357 | UniversalIoError::Bincode(_)
358 | UniversalIoError::BytemuckCast(_)
359 | UniversalIoError::ZerocopySize(_)
360 | UniversalIoError::IoUringNotSupported(_)
361 | UniversalIoError::OutOfBounds { .. }
362 | UniversalIoError::InvalidFileIndex { .. }
363 | UniversalIoError::Uninitialized { .. }
364 | UniversalIoError::QueueIsFull
365 | UniversalIoError::AppendOffsetConflict { .. }
366 | UniversalIoError::S3(_)
367 | UniversalIoError::S3Config { .. }
368 | UniversalIoError::TaskPanicked(_)) => {
369 Self::service_error(format!("Gridstore IO error: {err}"))
370 }
371 },
372 BlobstoreError::PageNotFound { .. } => Self::service_error(err.to_string()),
373 BlobstoreError::ValueNotFound { .. } => Self::service_error(err.to_string()),
374 }
375 }
376}
377
378#[cfg(feature = "gpu")]
379impl From<gpu::GpuError> for OperationError {
380 fn from(err: gpu::GpuError) -> Self {
381 Self::service_error(format!("GPU error: {err:?}"))
382 }
383}
384
385pub type OperationResult<T> = Result<T, OperationError>;
386
387pub fn get_service_error<T>(err: &OperationResult<T>) -> Option<OperationError> {
388 match err {
389 Ok(_) => None,
390 #[expect(clippy::wildcard_enum_match_arm, reason = "error handling")]
391 Err(error) => match error {
392 OperationError::ServiceError { .. } => Some(error.clone()),
393 _ => None,
394 },
395 }
396}
397
398#[derive(Debug, Copy, Clone)]
399pub struct CancelledError;
400
401pub type CancellableResult<T> = Result<T, CancelledError>;
402
403impl From<CancelledError> for OperationError {
404 fn from(_cancelled_error: CancelledError) -> Self {
405 Self::cancelled(PROCESS_CANCELLED_BY_SERVICE_MESSAGE)
406 }
407}
408
409pub fn check_process_stopped(stopped: &AtomicBool) -> CancellableResult<()> {
410 if stopped.load(Ordering::Relaxed) {
411 return Err(CancelledError);
412 }
413 Ok(())
414}
415
416#[cfg(test)]
417mod tests {
418 use std::time::Duration;
419
420 use super::*;
421
422 #[test]
423 fn test_not_found_classification() {
424 let err = OperationError::from(UniversalIoError::NotFound {
426 path: "segments/0/deleted.bin".into(),
427 });
428 assert!(err.is_not_found());
429 assert!(err.to_string().contains("segments/0/deleted.bin"));
430
431 let err = OperationError::from(MmapError::MissingFile("matrix.dat".to_string()));
432 assert!(err.is_not_found());
433 assert!(err.to_string().contains("matrix.dat"));
434
435 let err = OperationError::from(BlobstoreError::UniversalIo(UniversalIoError::NotFound {
436 path: "page_0.dat".into(),
437 }));
438 assert!(err.is_not_found());
439 assert!(err.to_string().contains("page_0.dat"));
440
441 let io_err = IoError::new(ErrorKind::NotFound, "no such file");
444 assert!(!OperationError::from(io_err).is_not_found());
445
446 let io_err = IoError::new(ErrorKind::PermissionDenied, "denied");
448 let err = OperationError::from(UniversalIoError::Io(io_err));
449 assert!(!err.is_not_found());
450 }
451
452 #[test]
453 fn test_timeout_error_formatting() {
454 let timeout = Duration::from_millis(500);
456 let error = OperationError::timeout(timeout, "test operation");
457 let error_msg = error.to_string();
458 assert!(
459 error_msg.contains("500ms"),
460 "Expected '500ms' but got: {error_msg}"
461 );
462
463 let timeout = Duration::from_millis(1000);
465 let error = OperationError::timeout(timeout, "test operation");
466 let error_msg = error.to_string();
467 assert!(
468 error_msg.contains("1s"),
469 "Expected '1s' but got: {error_msg}"
470 );
471
472 let timeout = Duration::from_millis(2500);
474 let error = OperationError::timeout(timeout, "test operation");
475 let error_msg = error.to_string();
476 assert!(
477 error_msg.contains("2.5s"),
478 "Expected '2.5s' but got: {error_msg}"
479 );
480
481 let timeout = Duration::from_millis(60000);
483 let error = OperationError::timeout(timeout, "test operation");
484 let error_msg = error.to_string();
485 assert!(
486 error_msg.contains("60s"),
487 "Expected '60s' but got: {error_msg}"
488 );
489 }
490}