Skip to content

Commit 59a5843

Browse files
committed
Add snafu error handling
1 parent a95e146 commit 59a5843

27 files changed

Lines changed: 585 additions & 201 deletions

Cargo.lock

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ reqwest = { version = "0.12", features = ["json", "blocking", "multipart"] }
3232
rocksdb = "0.21.0"
3333
serde = { version = "1.0", features = ["derive"] }
3434
serde_json = "1.0.145"
35+
snafu = "0.8.9"
3536
tempfile = "3.23.0"
3637
tokio = { version = "1.47.1", features = ["full"] }
3738
tokio-stream = "0.1.17"

crates/api/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ license.workspace = true
99
[dependencies]
1010
defs.workspace = true
1111
index.workspace = true
12+
snafu.workspace = true
1213
storage.workspace = true
1314
tempfile.workspace = true
1415
uuid.workspace = true

crates/api/src/error.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use defs::{Dimension, PointId};
2+
use snafu::prelude::*;
3+
4+
#[derive(Debug, Snafu)]
5+
#[snafu(visibility(pub))]
6+
pub enum ApiError {
7+
#[snafu(display("Vector dimension mismatch: expected {expected}, got {got}"))]
8+
DimensionMismatch { expected: Dimension, got: Dimension },
9+
10+
#[snafu(display("Failed to acquire lock on vector index"))]
11+
LockError,
12+
13+
#[snafu(display("Storage error: {source}"))]
14+
Storage {
15+
source: storage::error::StorageError,
16+
},
17+
18+
#[snafu(display("Index error: {source}"))]
19+
Index { source: index::error::IndexError },
20+
21+
#[snafu(display("Point {id} not found"))]
22+
PointNotFound { id: PointId },
23+
24+
#[snafu(display("Invalid search limit: {limit}"))]
25+
InvalidSearchLimit { limit: usize },
26+
27+
#[snafu(display("Failed to initialize database: {reason}"))]
28+
InitializationFailed { reason: String },
29+
}
30+
31+
pub type Result<T, E = ApiError> = std::result::Result<T, E>;
32+
33+
// Automatic conversion from StorageError to ApiError
34+
impl From<storage::error::StorageError> for ApiError {
35+
fn from(source: storage::error::StorageError) -> Self {
36+
ApiError::Storage { source }
37+
}
38+
}
39+
40+
// Automatic conversion from IndexError to ApiError
41+
impl From<index::error::IndexError> for ApiError {
42+
fn from(source: index::error::IndexError) -> Self {
43+
ApiError::Index { source }
44+
}
45+
}

crates/api/src/lib.rs

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use defs::{DbError, Dimension, IndexedVector, Similarity};
1+
use defs::{Dimension, IndexedVector, Similarity};
22

33
use defs::{DenseVector, Payload, Point, PointId};
44
use index::hnsw::HnswIndex;
@@ -13,6 +13,9 @@ use storage::{StorageEngine, StorageType, VectorPage};
1313

1414
use uuid::Uuid;
1515

16+
pub mod error;
17+
pub use error::{ApiError, Result};
18+
1619
// static NEXT_ID: AtomicU64 = AtomicU64::new(1);
1720

1821
// fn generate_point_id() -> u64 {
@@ -43,17 +46,20 @@ impl VectorDb {
4346
}
4447

4548
//TODO: Make this an atomic operation
46-
pub fn insert(&self, vector: DenseVector, payload: Payload) -> Result<PointId, DbError> {
49+
pub fn insert(&self, vector: DenseVector, payload: Payload) -> Result<PointId> {
4750
if vector.len() != self.dimension {
48-
return Err(DbError::DimensionMismatch);
51+
return Err(ApiError::DimensionMismatch {
52+
expected: self.dimension,
53+
got: vector.len(),
54+
});
4955
}
5056
// Generate a new point id
5157
let point_id = generate_point_id();
5258
self.storage
5359
.insert_point(point_id, Some(vector.clone()), Some(payload))?;
5460

5561
// Get write lock on the index
56-
let mut index = self.index.write().map_err(|_| DbError::LockError)?;
62+
let mut index = self.index.write().map_err(|_| ApiError::LockError)?;
5763
index.insert(IndexedVector {
5864
vector,
5965
id: point_id,
@@ -63,16 +69,16 @@ impl VectorDb {
6369
}
6470

6571
//TODO: Make this an atomic operation
66-
pub fn delete(&self, id: PointId) -> Result<bool, DbError> {
72+
pub fn delete(&self, id: PointId) -> Result<bool> {
6773
// Remove from storage
6874
self.storage.delete_point(id)?;
6975
// Remove from index
70-
let mut index = self.index.write().map_err(|_| DbError::LockError)?;
76+
let mut index = self.index.write().map_err(|_| ApiError::LockError)?;
7177
let point_found = index.delete(id)?;
7278
Ok(point_found)
7379
}
7480

75-
pub fn get(&self, id: PointId) -> Result<Option<Point>, DbError> {
81+
pub fn get(&self, id: PointId) -> Result<Option<Point>> {
7682
// Search for the Point with given id in storage
7783
let payload = self.storage.get_payload(id)?;
7884
let vector = self.storage.get_vector(id)?;
@@ -92,28 +98,42 @@ impl VectorDb {
9298
query: DenseVector,
9399
similarity: Similarity,
94100
limit: usize,
95-
) -> Result<Vec<PointId>, DbError> {
101+
) -> Result<Vec<PointId>> {
102+
// Validate search limit
103+
if limit == 0 {
104+
return Err(ApiError::InvalidSearchLimit { limit });
105+
}
106+
107+
// Validate query dimension
108+
if query.len() != self.dimension {
109+
return Err(ApiError::DimensionMismatch {
110+
expected: self.dimension,
111+
got: query.len(),
112+
});
113+
}
114+
96115
// Use vector index to find similar vectors
97-
let index = self.index.read().map_err(|_| DbError::LockError)?;
116+
let index = self.index.read().map_err(|_| ApiError::LockError)?;
98117

99118
//TODO: Add feat of returning similarity scores in the search
100119
let vectors = index.search(query, similarity, limit)?;
101120

102121
Ok(vectors)
103122
}
104123

105-
pub fn list(&self, offset: PointId, limit: usize) -> Result<Option<VectorPage>, DbError> {
106-
self.storage.list_vectors(offset, limit)
124+
pub fn list(&self, offset: PointId, limit: usize) -> Result<Option<VectorPage>> {
125+
let page = self.storage.list_vectors(offset, limit)?;
126+
Ok(page)
107127
}
108128

109129
// populates the current index with vectors from the storage
110-
pub fn build_index(&self) -> Result<usize, DbError> {
130+
pub fn build_index(&self) -> Result<usize> {
111131
// start from the minimal UUID and fetch in bounded batches and insert
112132
let mut offset = Uuid::nil();
113133
let page_size: usize = 1000;
114134
let mut inserted: usize = 0;
115135

116-
let mut index = self.index.write().map_err(|_| DbError::LockError)?;
136+
let mut index = self.index.write().map_err(|_| ApiError::LockError)?;
117137

118138
while let Some((batch, next_offset)) = self.storage.list_vectors(offset, page_size)? {
119139
if batch.is_empty() || next_offset == offset {
@@ -141,7 +161,7 @@ pub struct DbConfig {
141161
pub similarity: Similarity,
142162
}
143163

144-
pub fn init_api(config: DbConfig) -> Result<VectorDb, DbError> {
164+
pub fn init_api(config: DbConfig) -> Result<VectorDb> {
145165
// Initialize the storage engine
146166
let storage = match config.storage_type {
147167
StorageType::RocksDb => Arc::new(RocksDbStorage::new(config.data_path)?),
@@ -230,7 +250,13 @@ mod tests {
230250
// Insert vector of dimension 2 != 3
231251
let res2 = db.insert(v2, payload);
232252
assert!(res2.is_err());
233-
assert_eq!(res2.unwrap_err(), DbError::DimensionMismatch);
253+
match res2.unwrap_err() {
254+
ApiError::DimensionMismatch { expected, got } => {
255+
assert_eq!(expected, 3);
256+
assert_eq!(got, 2);
257+
}
258+
other => panic!("Expected DimensionMismatch, got: {:?}", other),
259+
}
234260
}
235261

236262
#[test]
@@ -312,6 +338,22 @@ mod tests {
312338
assert_eq!(results.len(), 3);
313339
}
314340

341+
#[test]
342+
fn test_search_zero_limit() {
343+
let (db, _temp_dir) = create_test_db();
344+
345+
let query = vec![1.0, 2.0, 3.0];
346+
let result = db.search(query, Similarity::Cosine, 0);
347+
348+
assert!(result.is_err());
349+
match result.unwrap_err() {
350+
ApiError::InvalidSearchLimit { limit } => {
351+
assert_eq!(limit, 0);
352+
}
353+
other => panic!("Expected InvalidSearchLimit, got: {:?}", other),
354+
}
355+
}
356+
315357
#[test]
316358
fn test_empty_database() {
317359
let (db, _temp_dir) = create_test_db();

crates/defs/src/error.rs

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,5 @@
11
use std::io;
22

3-
use crate::{Dimension, PointId};
4-
#[derive(Debug, PartialEq, Eq)]
5-
pub enum DbError {
6-
ParseError,
7-
StorageError(String),
8-
SerializationError(String),
9-
DeserializationError,
10-
IndexError(String),
11-
LockError,
12-
IndexInitError, //TODO: Change this
13-
UnsupportedSimilarity,
14-
DimensionMismatch,
15-
InvalidDimension { expected: Dimension, got: Dimension },
16-
PointAlreadyExists { id: PointId },
17-
PointNotFound { id: PointId },
18-
}
19-
203
#[derive(Debug)]
214
pub enum ServerError {
225
Bind(io::Error),
@@ -25,17 +8,8 @@ pub enum ServerError {
258

269
#[derive(Debug)]
2710
pub enum AppError {
28-
DbError(DbError),
2911
ServerError(ServerError),
3012
}
3113

32-
impl std::fmt::Display for DbError {
33-
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
34-
write!(f, "{:?}", self)
35-
}
36-
}
37-
38-
impl std::error::Error for DbError {}
39-
4014
// Error type for server
4115
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;

crates/grpc/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ defs.workspace = true
1212
index.workspace = true
1313
prost.workspace = true
1414
prost-types.workspace = true
15+
snafu.workspace = true
1516
storage.workspace = true
1617
tempfile.workspace = true
1718
tokio.workspace = true

0 commit comments

Comments
 (0)