Skip to main content

storage/
memory.rs

1use async_trait::async_trait;
2use common::{DakeraError, NamespaceId, Result, Vector, VectorId};
3use parking_lot::RwLock;
4use std::collections::HashMap;
5use std::sync::Arc;
6
7use crate::traits::VectorStorage;
8
9/// In-memory storage implementation for Phase 1
10/// Uses RwLock for concurrent access, wrapped in Arc for Clone support
11#[derive(Clone)]
12pub struct InMemoryStorage {
13    /// namespace -> (vector_id -> vector)
14    namespaces: Arc<RwLock<HashMap<NamespaceId, HashMap<VectorId, Vector>>>>,
15}
16
17impl InMemoryStorage {
18    pub fn new() -> Self {
19        Self {
20            namespaces: Arc::new(RwLock::new(HashMap::new())),
21        }
22    }
23}
24
25impl Default for InMemoryStorage {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31/// Capture current Unix timestamp in seconds — used to avoid N syscalls per filter loop.
32fn now_secs() -> u64 {
33    std::time::SystemTime::now()
34        .duration_since(std::time::UNIX_EPOCH)
35        .unwrap_or_default()
36        .as_secs()
37}
38
39#[async_trait]
40impl VectorStorage for InMemoryStorage {
41    async fn upsert(&self, namespace: &NamespaceId, vectors: Vec<Vector>) -> Result<usize> {
42        let mut namespaces = self.namespaces.write();
43        let ns = namespaces.entry(namespace.clone()).or_default();
44
45        // Validate dimensions: check against existing vectors OR ensure intra-batch consistency
46        if !vectors.is_empty() {
47            let now = now_secs();
48            let expected_dim = ns
49                .values()
50                .find(|v| !v.is_expired_at(now))
51                .map(|v| v.values.len())
52                .unwrap_or_else(|| vectors[0].values.len());
53            for v in &vectors {
54                if v.values.len() != expected_dim {
55                    return Err(DakeraError::DimensionMismatch {
56                        expected: expected_dim,
57                        actual: v.values.len(),
58                    });
59                }
60            }
61        }
62
63        let count = vectors.len();
64        for mut vector in vectors {
65            // Apply TTL if specified
66            vector.apply_ttl();
67            ns.insert(vector.id.clone(), vector);
68        }
69
70        tracing::debug!(
71            namespace = %namespace,
72            count = count,
73            "Upserted vectors"
74        );
75
76        Ok(count)
77    }
78
79    async fn get(&self, namespace: &NamespaceId, ids: &[VectorId]) -> Result<Vec<Vector>> {
80        let namespaces = self.namespaces.read();
81        let ns = namespaces
82            .get(namespace)
83            .ok_or_else(|| DakeraError::NamespaceNotFound(namespace.clone()))?;
84
85        // Filter out expired vectors — check expiry on the reference before cloning
86        // to avoid heap-allocating Vec<f32> for entries we're about to discard.
87        let now = now_secs();
88        Ok(ids
89            .iter()
90            .filter_map(|id| ns.get(id).filter(|v| !v.is_expired_at(now)).cloned())
91            .collect())
92    }
93
94    async fn get_all(&self, namespace: &NamespaceId) -> Result<Vec<Vector>> {
95        let namespaces = self.namespaces.read();
96        let ns = namespaces
97            .get(namespace)
98            .ok_or_else(|| DakeraError::NamespaceNotFound(namespace.clone()))?;
99
100        // Filter out expired vectors
101        let now = now_secs();
102        Ok(ns
103            .values()
104            .filter(|v| !v.is_expired_at(now))
105            .cloned()
106            .collect())
107    }
108
109    async fn get_all_meta(&self, namespace: &NamespaceId) -> Result<Vec<Vector>> {
110        let namespaces = self.namespaces.read();
111        let ns = namespaces
112            .get(namespace)
113            .ok_or_else(|| DakeraError::NamespaceNotFound(namespace.clone()))?;
114
115        // DAK-7387: project under the read lock without cloning the embedding
116        // values — expiry filtering mirrors get_all exactly.
117        let now = now_secs();
118        Ok(ns
119            .values()
120            .filter(|v| !v.is_expired_at(now))
121            .map(|v| Vector {
122                id: v.id.clone(),
123                values: Vec::new(),
124                metadata: v.metadata.clone(),
125                ttl_seconds: v.ttl_seconds,
126                expires_at: v.expires_at,
127            })
128            .collect())
129    }
130
131    async fn delete(&self, namespace: &NamespaceId, ids: &[VectorId]) -> Result<usize> {
132        let mut namespaces = self.namespaces.write();
133        let ns = namespaces
134            .get_mut(namespace)
135            .ok_or_else(|| DakeraError::NamespaceNotFound(namespace.clone()))?;
136
137        let mut deleted = 0;
138        for id in ids {
139            if ns.remove(id).is_some() {
140                deleted += 1;
141            }
142        }
143
144        tracing::debug!(
145            namespace = %namespace,
146            deleted = deleted,
147            "Deleted vectors"
148        );
149
150        Ok(deleted)
151    }
152
153    async fn namespace_exists(&self, namespace: &NamespaceId) -> Result<bool> {
154        Ok(self.namespaces.read().contains_key(namespace))
155    }
156
157    async fn ensure_namespace(&self, namespace: &NamespaceId) -> Result<()> {
158        self.namespaces
159            .write()
160            .entry(namespace.clone())
161            .or_default();
162        Ok(())
163    }
164
165    async fn count(&self, namespace: &NamespaceId) -> Result<usize> {
166        let namespaces = self.namespaces.read();
167        let now = now_secs();
168        Ok(namespaces
169            .get(namespace)
170            .map(|ns| ns.values().filter(|v| !v.is_expired_at(now)).count())
171            .unwrap_or(0))
172    }
173
174    async fn dimension(&self, namespace: &NamespaceId) -> Result<Option<usize>> {
175        let namespaces = self.namespaces.read();
176        let now = now_secs();
177        Ok(namespaces
178            .get(namespace)
179            .and_then(|ns| ns.values().find(|v| !v.is_expired_at(now)))
180            .map(|v| v.values.len()))
181    }
182
183    async fn list_namespaces(&self) -> Result<Vec<NamespaceId>> {
184        let namespaces = self.namespaces.read();
185        Ok(namespaces.keys().cloned().collect())
186    }
187
188    async fn delete_namespace(&self, namespace: &NamespaceId) -> Result<bool> {
189        let mut namespaces = self.namespaces.write();
190        let existed = namespaces.remove(namespace).is_some();
191
192        if existed {
193            tracing::debug!(
194                namespace = %namespace,
195                "Deleted namespace"
196            );
197        }
198
199        Ok(existed)
200    }
201
202    async fn cleanup_expired(&self, namespace: &NamespaceId) -> Result<usize> {
203        let mut namespaces = self.namespaces.write();
204        let ns = match namespaces.get_mut(namespace) {
205            Some(ns) => ns,
206            None => return Ok(0),
207        };
208
209        let now = now_secs();
210        let before_count = ns.len();
211        ns.retain(|_, v| !v.is_expired_at(now));
212        let removed = before_count - ns.len();
213
214        if removed > 0 {
215            tracing::debug!(
216                namespace = %namespace,
217                removed = removed,
218                "Cleaned up expired vectors"
219            );
220        }
221
222        Ok(removed)
223    }
224
225    async fn cleanup_all_expired(&self) -> Result<usize> {
226        let mut namespaces = self.namespaces.write();
227        let mut total_removed = 0;
228
229        let now = now_secs();
230        for (namespace, ns) in namespaces.iter_mut() {
231            let before_count = ns.len();
232            ns.retain(|_, v| !v.is_expired_at(now));
233            let removed = before_count - ns.len();
234            total_removed += removed;
235
236            if removed > 0 {
237                tracing::debug!(
238                    namespace = %namespace,
239                    removed = removed,
240                    "Cleaned up expired vectors"
241                );
242            }
243        }
244
245        if total_removed > 0 {
246            tracing::info!(
247                total_removed = total_removed,
248                "Cleaned up expired vectors across all namespaces"
249            );
250        }
251
252        Ok(total_removed)
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[tokio::test]
261    async fn test_upsert_and_get() {
262        let storage = InMemoryStorage::new();
263        let namespace = "test".to_string();
264
265        let vectors = vec![Vector {
266            id: "v1".to_string(),
267            values: vec![1.0, 2.0, 3.0],
268            metadata: None,
269            ttl_seconds: None,
270            expires_at: None,
271        }];
272
273        storage.ensure_namespace(&namespace).await.unwrap();
274        let count = storage.upsert(&namespace, vectors).await.unwrap();
275        assert_eq!(count, 1);
276
277        let retrieved = storage.get(&namespace, &["v1".to_string()]).await.unwrap();
278        assert_eq!(retrieved.len(), 1);
279        assert_eq!(retrieved[0].id, "v1");
280    }
281
282    #[tokio::test]
283    async fn test_dimension_mismatch() {
284        let storage = InMemoryStorage::new();
285        let namespace = "test".to_string();
286
287        storage.ensure_namespace(&namespace).await.unwrap();
288
289        // First vector sets dimension
290        storage
291            .upsert(
292                &namespace,
293                vec![Vector {
294                    id: "v1".to_string(),
295                    values: vec![1.0, 2.0, 3.0],
296                    metadata: None,
297                    ttl_seconds: None,
298                    expires_at: None,
299                }],
300            )
301            .await
302            .unwrap();
303
304        // Second vector has wrong dimension
305        let result = storage
306            .upsert(
307                &namespace,
308                vec![Vector {
309                    id: "v2".to_string(),
310                    values: vec![1.0, 2.0],
311                    metadata: None,
312                    ttl_seconds: None,
313                    expires_at: None,
314                }],
315            )
316            .await;
317
318        assert!(matches!(result, Err(DakeraError::DimensionMismatch { .. })));
319    }
320
321    #[tokio::test]
322    async fn test_delete() {
323        let storage = InMemoryStorage::new();
324        let namespace = "test".to_string();
325
326        storage.ensure_namespace(&namespace).await.unwrap();
327        storage
328            .upsert(
329                &namespace,
330                vec![
331                    Vector {
332                        id: "v1".to_string(),
333                        values: vec![1.0],
334                        metadata: None,
335                        ttl_seconds: None,
336                        expires_at: None,
337                    },
338                    Vector {
339                        id: "v2".to_string(),
340                        values: vec![2.0],
341                        metadata: None,
342                        ttl_seconds: None,
343                        expires_at: None,
344                    },
345                ],
346            )
347            .await
348            .unwrap();
349
350        let deleted = storage
351            .delete(&namespace, &["v1".to_string()])
352            .await
353            .unwrap();
354        assert_eq!(deleted, 1);
355
356        let count = storage.count(&namespace).await.unwrap();
357        assert_eq!(count, 1);
358    }
359
360    #[tokio::test]
361    async fn test_get_all() {
362        let storage = InMemoryStorage::new();
363        let namespace = "test".to_string();
364
365        storage.ensure_namespace(&namespace).await.unwrap();
366        storage
367            .upsert(
368                &namespace,
369                vec![
370                    Vector {
371                        id: "v1".to_string(),
372                        values: vec![1.0, 2.0],
373                        metadata: None,
374                        ttl_seconds: None,
375                        expires_at: None,
376                    },
377                    Vector {
378                        id: "v2".to_string(),
379                        values: vec![3.0, 4.0],
380                        metadata: None,
381                        ttl_seconds: None,
382                        expires_at: None,
383                    },
384                ],
385            )
386            .await
387            .unwrap();
388
389        let all = storage.get_all(&namespace).await.unwrap();
390        assert_eq!(all.len(), 2);
391    }
392
393    #[tokio::test]
394    async fn test_ttl_expired_vectors_filtered() {
395        let storage = InMemoryStorage::new();
396        let namespace = "test".to_string();
397
398        storage.ensure_namespace(&namespace).await.unwrap();
399
400        // Insert a vector that's already expired (expires_at in the past)
401        let past_timestamp = std::time::SystemTime::now()
402            .duration_since(std::time::UNIX_EPOCH)
403            .unwrap()
404            .as_secs()
405            - 100; // 100 seconds ago
406
407        storage
408            .upsert(
409                &namespace,
410                vec![
411                    Vector {
412                        id: "expired".to_string(),
413                        values: vec![1.0, 2.0],
414                        metadata: None,
415                        ttl_seconds: None,
416                        expires_at: Some(past_timestamp),
417                    },
418                    Vector {
419                        id: "valid".to_string(),
420                        values: vec![3.0, 4.0],
421                        metadata: None,
422                        ttl_seconds: None,
423                        expires_at: None,
424                    },
425                ],
426            )
427            .await
428            .unwrap();
429
430        // get() should only return the valid vector
431        let retrieved = storage
432            .get(&namespace, &["expired".to_string(), "valid".to_string()])
433            .await
434            .unwrap();
435        assert_eq!(retrieved.len(), 1);
436        assert_eq!(retrieved[0].id, "valid");
437
438        // get_all() should only return the valid vector
439        let all = storage.get_all(&namespace).await.unwrap();
440        assert_eq!(all.len(), 1);
441        assert_eq!(all[0].id, "valid");
442
443        // count() should only count the valid vector
444        let count = storage.count(&namespace).await.unwrap();
445        assert_eq!(count, 1);
446    }
447
448    #[tokio::test]
449    async fn test_ttl_applied_on_upsert() {
450        let storage = InMemoryStorage::new();
451        let namespace = "test".to_string();
452
453        storage.ensure_namespace(&namespace).await.unwrap();
454
455        // Insert a vector with TTL
456        storage
457            .upsert(
458                &namespace,
459                vec![Vector {
460                    id: "with_ttl".to_string(),
461                    values: vec![1.0, 2.0],
462                    metadata: None,
463                    ttl_seconds: Some(3600), // 1 hour TTL
464                    expires_at: None,
465                }],
466            )
467            .await
468            .unwrap();
469
470        // The vector should have expires_at set
471        let namespaces = storage.namespaces.read();
472        let ns = namespaces.get(&namespace).unwrap();
473        let vector = ns.get("with_ttl").unwrap();
474        assert!(vector.expires_at.is_some());
475
476        // expires_at should be roughly 1 hour from now
477        let now = std::time::SystemTime::now()
478            .duration_since(std::time::UNIX_EPOCH)
479            .unwrap()
480            .as_secs();
481        let expires_at = vector.expires_at.unwrap();
482        assert!(expires_at > now);
483        assert!(expires_at <= now + 3601); // Allow 1 second tolerance
484    }
485
486    #[tokio::test]
487    async fn test_cleanup_expired() {
488        let storage = InMemoryStorage::new();
489        let namespace = "test".to_string();
490
491        storage.ensure_namespace(&namespace).await.unwrap();
492
493        let past_timestamp = std::time::SystemTime::now()
494            .duration_since(std::time::UNIX_EPOCH)
495            .unwrap()
496            .as_secs()
497            - 100;
498
499        storage
500            .upsert(
501                &namespace,
502                vec![
503                    Vector {
504                        id: "expired1".to_string(),
505                        values: vec![1.0],
506                        metadata: None,
507                        ttl_seconds: None,
508                        expires_at: Some(past_timestamp),
509                    },
510                    Vector {
511                        id: "expired2".to_string(),
512                        values: vec![2.0],
513                        metadata: None,
514                        ttl_seconds: None,
515                        expires_at: Some(past_timestamp),
516                    },
517                    Vector {
518                        id: "valid".to_string(),
519                        values: vec![3.0],
520                        metadata: None,
521                        ttl_seconds: None,
522                        expires_at: None,
523                    },
524                ],
525            )
526            .await
527            .unwrap();
528
529        // Before cleanup, raw storage has 3 vectors
530        {
531            let namespaces = storage.namespaces.read();
532            let ns = namespaces.get(&namespace).unwrap();
533            assert_eq!(ns.len(), 3);
534        }
535
536        // Cleanup should remove 2 expired vectors
537        let removed = storage.cleanup_expired(&namespace).await.unwrap();
538        assert_eq!(removed, 2);
539
540        // After cleanup, storage has 1 vector
541        {
542            let namespaces = storage.namespaces.read();
543            let ns = namespaces.get(&namespace).unwrap();
544            assert_eq!(ns.len(), 1);
545            assert!(ns.contains_key("valid"));
546        }
547    }
548
549    #[tokio::test]
550    async fn test_cleanup_all_expired() {
551        let storage = InMemoryStorage::new();
552        let ns1 = "test1".to_string();
553        let ns2 = "test2".to_string();
554
555        storage.ensure_namespace(&ns1).await.unwrap();
556        storage.ensure_namespace(&ns2).await.unwrap();
557
558        let past_timestamp = std::time::SystemTime::now()
559            .duration_since(std::time::UNIX_EPOCH)
560            .unwrap()
561            .as_secs()
562            - 100;
563
564        // Add expired vectors to both namespaces
565        storage
566            .upsert(
567                &ns1,
568                vec![Vector {
569                    id: "expired".to_string(),
570                    values: vec![1.0],
571                    metadata: None,
572                    ttl_seconds: None,
573                    expires_at: Some(past_timestamp),
574                }],
575            )
576            .await
577            .unwrap();
578
579        storage
580            .upsert(
581                &ns2,
582                vec![
583                    Vector {
584                        id: "expired".to_string(),
585                        values: vec![2.0],
586                        metadata: None,
587                        ttl_seconds: None,
588                        expires_at: Some(past_timestamp),
589                    },
590                    Vector {
591                        id: "valid".to_string(),
592                        values: vec![3.0],
593                        metadata: None,
594                        ttl_seconds: None,
595                        expires_at: None,
596                    },
597                ],
598            )
599            .await
600            .unwrap();
601
602        // Cleanup all should remove 2 expired vectors total
603        let removed = storage.cleanup_all_expired().await.unwrap();
604        assert_eq!(removed, 2);
605
606        // ns1 should be empty, ns2 should have 1 valid vector
607        {
608            let namespaces = storage.namespaces.read();
609            assert_eq!(namespaces.get(&ns1).unwrap().len(), 0);
610            assert_eq!(namespaces.get(&ns2).unwrap().len(), 1);
611        }
612    }
613
614    /// Verify that get() filters expired vectors without cloning them (P2 optimization):
615    /// expired vectors must not appear in results even when mixed with live ones.
616    #[tokio::test]
617    async fn test_get_skips_expired_without_clone() {
618        let storage = InMemoryStorage::new();
619        let namespace = "expire-test".to_string();
620        storage.ensure_namespace(&namespace).await.unwrap();
621
622        let past = now_secs().saturating_sub(1); // already expired
623        let future = now_secs() + 3600;
624
625        storage
626            .upsert(
627                &namespace,
628                vec![
629                    Vector {
630                        id: "live".to_string(),
631                        values: vec![1.0, 0.0],
632                        metadata: None,
633                        ttl_seconds: None,
634                        expires_at: Some(future),
635                    },
636                    Vector {
637                        id: "dead".to_string(),
638                        values: vec![0.0, 1.0],
639                        metadata: None,
640                        ttl_seconds: None,
641                        expires_at: Some(past),
642                    },
643                ],
644            )
645            .await
646            .unwrap();
647
648        let results = storage
649            .get(&namespace, &["live".to_string(), "dead".to_string()])
650            .await
651            .unwrap();
652
653        assert_eq!(
654            results.len(),
655            1,
656            "expired vector must be filtered before clone"
657        );
658        assert_eq!(results[0].id, "live");
659    }
660}