Skip to content

Commit 363055b

Browse files
authored
Inmemory storage implementation (#82)
* Implement in-memory storage engine * Wire in-memory storage through API snapshots * Add HTTP smoke coverage for in-memory storage * Handle in-memory storage errors in gRPC * Add in-memory checkpoint header
1 parent 66d331f commit 363055b

10 files changed

Lines changed: 855 additions & 35 deletions

File tree

‎Cargo.lock‎

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

‎crates/api/src/lib.rs‎

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ use std::sync::{Arc, RwLock};
1010
use index::flat::index::FlatIndex;
1111
use index::{IndexType, VectorIndex};
1212
use snapshot::Snapshot;
13-
use storage::rocks_db::RocksDbStorage;
14-
use storage::{StorageEngine, StorageType, VectorPage};
13+
use storage::{StorageEngine, StorageType, VectorPage, create_storage_engine};
1514

1615
use uuid::Uuid;
1716

@@ -252,10 +251,7 @@ pub fn restore_from_snapshot(config: &DbRestoreConfig) -> Result<VectorDb, DbErr
252251

253252
pub fn init_api(config: DbConfig) -> Result<VectorDb> {
254253
// Initialize the storage engine
255-
let storage = match config.storage_type {
256-
StorageType::RocksDb => Arc::new(RocksDbStorage::new(config.data_path)?),
257-
_ => Arc::new(RocksDbStorage::new(config.data_path)?),
258-
};
254+
let storage = create_storage_engine(config.storage_type, config.data_path)?;
259255

260256
// Initialize the vector index
261257
let index: Arc<RwLock<dyn VectorIndex>> = match config.index_type {
@@ -290,9 +286,13 @@ mod tests {
290286

291287
// Helper function to create a test database
292288
fn create_test_db() -> (VectorDb, TempDir) {
289+
create_test_db_with_storage(StorageType::RocksDb)
290+
}
291+
292+
fn create_test_db_with_storage(storage_type: StorageType) -> (VectorDb, TempDir) {
293293
let temp_dir = tempdir().unwrap();
294294
let config = DbConfig {
295-
storage_type: StorageType::RocksDb,
295+
storage_type,
296296
index_type: IndexType::Flat,
297297
data_path: temp_dir.path().to_path_buf(),
298298
dimension: 3,
@@ -301,6 +301,13 @@ mod tests {
301301
(init_api(config).unwrap(), temp_dir)
302302
}
303303

304+
fn test_payload(content: &str) -> Payload {
305+
Payload {
306+
content_type: ContentType::Text,
307+
content: content.to_string(),
308+
}
309+
}
310+
304311
#[test]
305312
fn test_insert_and_get() {
306313
let (db, _temp_dir) = create_test_db();
@@ -326,6 +333,20 @@ mod tests {
326333
assert_eq!(point.payload.as_ref().unwrap().content, "Test content");
327334
}
328335

336+
#[test]
337+
fn test_insert_and_get_with_in_memory_storage() {
338+
let (db, _temp_dir) = create_test_db_with_storage(StorageType::InMemory);
339+
let vector = vec![1.0, 2.0, 3.0];
340+
let payload = test_payload("Test content");
341+
342+
let id = db.insert(vector.clone(), payload.clone()).unwrap();
343+
let point = db.get(id).unwrap().unwrap();
344+
345+
assert_eq!(point.id, id);
346+
assert_eq!(point.vector, Some(vector));
347+
assert_eq!(point.payload, Some(payload));
348+
}
349+
329350
#[test]
330351
fn test_dimension_mismatch() {
331352
let (db, _temp_dir) = create_test_db();
@@ -593,6 +614,34 @@ mod tests {
593614
assert!(loaded_db.get(id2).unwrap().unwrap().vector.unwrap() == v2);
594615
}
595616

617+
#[test]
618+
fn test_create_and_load_snapshot_with_in_memory_storage() {
619+
let (old_db, temp_dir) = create_test_db_with_storage(StorageType::InMemory);
620+
621+
let v1 = vec![0.0, 1.0, 2.0];
622+
let v2 = vec![3.0, 4.0, 5.0];
623+
let v3 = vec![6.0, 7.0, 8.0];
624+
625+
let id1 = old_db.insert(v1.clone(), test_payload("one")).unwrap();
626+
let id2 = old_db.insert(v2.clone(), test_payload("two")).unwrap();
627+
628+
let temp_snapshot_dir = tempdir().unwrap();
629+
let snapshot_path = old_db.create_snapshot(temp_snapshot_dir.path()).unwrap();
630+
631+
let id3 = old_db.insert(v3, test_payload("three")).unwrap();
632+
633+
let reload_config = DbRestoreConfig {
634+
data_path: temp_dir.path().to_path_buf(),
635+
snapshot_path,
636+
};
637+
638+
let loaded_db = restore_from_snapshot(&reload_config).unwrap();
639+
640+
assert_eq!(loaded_db.get(id1).unwrap().unwrap().vector, Some(v1));
641+
assert_eq!(loaded_db.get(id2).unwrap().unwrap().vector, Some(v2));
642+
assert!(loaded_db.get(id3).unwrap().is_none());
643+
}
644+
596645
#[test]
597646
fn test_snapshot_engine() {
598647
let (_db, _temp_dir) = create_test_db();

‎crates/grpc/src/error.rs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,15 @@ impl From<storage::error::StorageError> for GrpcError {
135135
StorageError::RocksDbFlush { source: _ } => GrpcError::Internal {
136136
message: "flush error".to_string(),
137137
},
138+
StorageError::InMemoryLock {} => GrpcError::Internal {
139+
message: "failed to lock in-memory storage".to_string(),
140+
},
141+
StorageError::InMemoryCheckpoint { msg } => GrpcError::Internal {
142+
message: format!("in-memory checkpoint error: {}", msg),
143+
},
144+
StorageError::InMemoryCheckpointIo { msg, source: _ } => GrpcError::Internal {
145+
message: format!("in-memory checkpoint io error: {}", msg),
146+
},
138147
}
139148
}
140149
}

‎crates/http/Cargo.toml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@ serde_json.workspace = true
1616
storage.workspace = true
1717
tokio.workspace = true
1818
tracing.workspace = true
19+
20+
[dev-dependencies]
21+
axum-test.workspace = true
22+
tempfile.workspace = true

‎crates/http/src/handler.rs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,10 @@ fn api_error_to_response(err: &ApiError) -> (StatusCode, String) {
149149
| StorageError::RocksDbFlush { .. }
150150
| StorageError::RocksDbInitialization { .. }
151151
| StorageError::RocksDbCheckpointMsg { .. }
152-
| StorageError::RocksDbCheckpointIo { .. } => {
152+
| StorageError::RocksDbCheckpointIo { .. }
153+
| StorageError::InMemoryLock { .. }
154+
| StorageError::InMemoryCheckpoint { .. }
155+
| StorageError::InMemoryCheckpointIo { .. } => {
153156
(StatusCode::INTERNAL_SERVER_ERROR, source.to_string())
154157
}
155158
},

‎crates/http/src/lib.rs‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,67 @@ pub async fn run_http_server(db: Arc<VectorDb>, addr: SocketAddr) -> Result<(),
4646
axum::serve(listener, app.into_make_service()).await?;
4747
Ok(())
4848
}
49+
50+
#[cfg(test)]
51+
mod tests {
52+
use super::*;
53+
use api::DbConfig;
54+
use axum::http::StatusCode;
55+
use axum_test::TestServer;
56+
use defs::Similarity;
57+
use index::IndexType;
58+
use serde_json::json;
59+
use storage::StorageType;
60+
61+
#[tokio::test]
62+
async fn in_memory_storage_http_smoke_test() {
63+
let temp_dir = tempfile::tempdir().unwrap();
64+
let db = api::init_api(DbConfig {
65+
storage_type: StorageType::InMemory,
66+
index_type: IndexType::Flat,
67+
data_path: temp_dir.path().to_path_buf(),
68+
dimension: 3,
69+
similarity: Similarity::Cosine,
70+
})
71+
.unwrap();
72+
let server = TestServer::new(create_router(Arc::new(db))).unwrap();
73+
74+
let insert_response = server
75+
.post("/points")
76+
.json(&json!({
77+
"vector": [1.0, 0.0, 0.0],
78+
"payload": {
79+
"content_type": "Text",
80+
"content": "smoke-test"
81+
}
82+
}))
83+
.await;
84+
insert_response.assert_status(StatusCode::CREATED);
85+
let insert_body: serde_json::Value = insert_response.json();
86+
let point_id = insert_body["point_id"].as_str().unwrap();
87+
88+
let get_response = server.get(&format!("/points/{point_id}")).await;
89+
get_response.assert_status_ok();
90+
let point_body: serde_json::Value = get_response.json();
91+
assert_eq!(point_body["payload"]["content"], "smoke-test");
92+
assert_eq!(point_body["vector"], json!([1.0, 0.0, 0.0]));
93+
94+
let search_response = server
95+
.post("/points/search")
96+
.json(&json!({
97+
"vector": [1.0, 0.0, 0.0],
98+
"similarity": "Cosine",
99+
"limit": 1
100+
}))
101+
.await;
102+
search_response.assert_status_ok();
103+
let search_body: serde_json::Value = search_response.json();
104+
assert_eq!(search_body["results"], json!([point_id]));
105+
106+
let delete_response = server.delete(&format!("/points/{point_id}")).await;
107+
delete_response.assert_status(StatusCode::NO_CONTENT);
108+
109+
let missing_response = server.get(&format!("/points/{point_id}")).await;
110+
missing_response.assert_status(StatusCode::NOT_FOUND);
111+
}
112+
}

‎crates/snapshot/src/lib.rs‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ use std::{
2626
time::SystemTime,
2727
};
2828
use storage::{
29-
StorageEngine, StorageType, checkpoint::StorageCheckpoint, rocks_db::RocksDbStorage,
29+
StorageEngine, StorageType, checkpoint::StorageCheckpoint, in_memory::MemoryStorage,
30+
rocks_db::RocksDbStorage,
3031
};
3132
use tar::Archive;
3233
use tempfile::tempdir;
@@ -201,17 +202,12 @@ impl Snapshot {
201202
));
202203
}
203204

204-
// only rocksdb is supported for snapshots as of now
205205
let mut storage_engine: Box<dyn StorageEngine> = match manifest.storage_type {
206+
StorageType::InMemory => Box::new(MemoryStorage::new()),
206207
StorageType::RocksDb => Box::new(
207208
RocksDbStorage::new(storage_data_path)
208209
.map_err(|e| DbError::StorageError(format!("Could not open storage: {e}")))?,
209210
),
210-
_ => {
211-
return Err(DbError::SnapshotError(
212-
"Unsupported storage type".to_string(),
213-
));
214-
}
215211
};
216212

217213
let id = manifest.id;

‎crates/storage/src/checkpoint.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ impl StorageCheckpoint {
3535
.0;
3636

3737
let storage_type = match marker {
38+
INMEMORY_CHECKPOINT_FILENAME_MARKER => StorageType::InMemory,
3839
ROCKSDB_CHECKPOINT_FILENAME_MARKER => StorageType::RocksDb,
3940
_ => {
4041
return Err(DbError::StorageCheckpointError(

‎crates/storage/src/error.rs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ pub enum StorageError {
3737
#[snafu(display("Failed to iterate over storage: {source}"))]
3838
RocksDbIteration { source: rocksdb::Error },
3939

40+
#[snafu(display("Failed to lock in-memory storage"))]
41+
InMemoryLock {},
42+
43+
#[snafu(display("In-memory checkpoint error: {}", msg))]
44+
InMemoryCheckpoint { msg: String },
45+
46+
#[snafu(display("{} : {}", msg, source))]
47+
InMemoryCheckpointIo { msg: String, source: std::io::Error },
48+
4049
#[snafu(display("Failed to serialize point {id}: {source}"))]
4150
Serialization { id: PointId, source: bincode::Error },
4251

0 commit comments

Comments
 (0)