1use crate::snapshot::{SnapshotConfig, SnapshotManager};
9use crate::traits::VectorStorage;
10use common::{DakeraError, NamespaceId, Result, Vector, VectorId};
11use parking_lot::RwLock;
12use serde::{Deserialize, Serialize};
13use std::fs::{self, File, OpenOptions};
14use std::io::{BufRead, BufReader, BufWriter, Write};
15use std::path::{Path, PathBuf};
16use std::sync::atomic::{AtomicU64, Ordering};
17
18#[derive(Debug, Clone)]
20pub struct WalConfig {
21 pub wal_dir: PathBuf,
23 pub max_segment_size: u64,
25 pub sync_mode: WalSyncMode,
27 pub checkpoint_threshold: u64,
29}
30
31impl Default for WalConfig {
32 fn default() -> Self {
33 Self {
34 wal_dir: PathBuf::from("./data/wal"),
35 max_segment_size: 64 * 1024 * 1024, sync_mode: WalSyncMode::EveryWrite,
37 checkpoint_threshold: 10000,
38 }
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum WalSyncMode {
45 EveryWrite,
47 Periodic(u32),
49 Manual,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub enum WalEntry {
56 Upsert {
58 namespace: String,
59 vectors: Vec<SerializedVector>,
60 },
61 Delete { namespace: String, ids: Vec<String> },
63 CreateNamespace { namespace: String },
65 DeleteNamespace { namespace: String },
67 Checkpoint { lsn: u64 },
69 TxnBegin { txn_id: u64 },
71 TxnCommit { txn_id: u64 },
73 TxnRollback { txn_id: u64 },
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct SerializedVector {
80 pub id: String,
81 pub values: Vec<f32>,
82 pub metadata: Option<String>,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub ttl_seconds: Option<u64>,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub expires_at: Option<u64>,
89}
90
91impl SerializedVector {
92 pub fn from_vector(v: &Vector) -> Self {
94 Self {
95 id: v.id.clone(),
96 values: v.values.clone(),
97 metadata: v.metadata.as_ref().map(|m| m.to_string()),
98 ttl_seconds: v.ttl_seconds,
99 expires_at: v.expires_at,
100 }
101 }
102
103 pub fn into_vector(self) -> Vector {
105 Vector {
106 id: self.id,
107 values: self.values,
108 metadata: self.metadata.and_then(|s| serde_json::from_str(&s).ok()),
109 ttl_seconds: self.ttl_seconds,
110 expires_at: self.expires_at,
111 }
112 }
113}
114
115#[derive(Debug)]
117struct WalSegment {
118 size: u64,
120 start_lsn: u64,
122 end_lsn: u64,
124 path: PathBuf,
129}
130
131pub struct WriteAheadLog {
133 config: WalConfig,
134 lsn: AtomicU64,
136 current_segment: RwLock<Option<WalSegment>>,
138 writer: RwLock<Option<BufWriter<File>>>,
140 entries_since_checkpoint: AtomicU64,
142 last_checkpoint_lsn: AtomicU64,
144 write_count: AtomicU64,
146 checkpoint_needed: tokio::sync::Notify,
153}
154
155impl WriteAheadLog {
156 pub fn new(config: WalConfig) -> Result<Self> {
158 fs::create_dir_all(&config.wal_dir)
160 .map_err(|e| DakeraError::Storage(format!("Failed to create WAL dir: {}", e)))?;
161
162 let wal = Self {
163 config,
164 lsn: AtomicU64::new(0),
165 current_segment: RwLock::new(None),
166 writer: RwLock::new(None),
167 entries_since_checkpoint: AtomicU64::new(0),
168 last_checkpoint_lsn: AtomicU64::new(0),
169 write_count: AtomicU64::new(0),
170 checkpoint_needed: tokio::sync::Notify::new(),
171 };
172
173 wal.recover_lsn()?;
175
176 Ok(wal)
177 }
178
179 fn recover_lsn(&self) -> Result<()> {
181 let segments = self.list_segments()?;
182 if let Some(last_segment) = segments.last() {
183 let entries = self.read_segment(last_segment)?;
185 if let Some(last_entry) = entries.last() {
186 self.lsn.store(last_entry.0 + 1, Ordering::SeqCst);
187 }
188 }
189 Ok(())
190 }
191
192 fn list_segments(&self) -> Result<Vec<PathBuf>> {
194 let mut segments = Vec::new();
195
196 if let Ok(entries) = fs::read_dir(&self.config.wal_dir) {
197 for entry in entries.flatten() {
198 let path = entry.path();
199 if path.extension().map(|e| e == "wal").unwrap_or(false) {
200 segments.push(path);
201 }
202 }
203 }
204
205 segments.sort();
206 Ok(segments)
207 }
208
209 fn read_segment(&self, path: &Path) -> Result<Vec<(u64, WalEntry)>> {
211 let file = File::open(path)
212 .map_err(|e| DakeraError::Storage(format!("Failed to open WAL: {}", e)))?;
213
214 let reader = BufReader::new(file);
215 let mut entries = Vec::new();
216
217 for line_result in reader.lines() {
218 let line =
219 line_result.map_err(|e| DakeraError::Storage(format!("WAL read error: {}", e)))?;
220
221 if line.trim().is_empty() {
222 continue;
223 }
224
225 if let Some((lsn_str, entry_json)) = line.split_once('|') {
227 let lsn: u64 = lsn_str.parse().unwrap_or(0);
228 if let Ok(entry) = serde_json::from_str::<WalEntry>(entry_json) {
229 entries.push((lsn, entry));
230 }
231 }
232 }
233
234 Ok(entries)
235 }
236
237 pub fn append(&self, entry: WalEntry) -> Result<u64> {
239 let lsn = self.lsn.fetch_add(1, Ordering::SeqCst);
240
241 self.ensure_segment()?;
243
244 let entry_json = serde_json::to_string(&entry)
246 .map_err(|e| DakeraError::Storage(format!("WAL serialize error: {}", e)))?;
247
248 let line = format!("{}|{}\n", lsn, entry_json);
249
250 {
251 let mut writer_guard = self.writer.write();
252 let writer = writer_guard.as_mut().ok_or_else(|| {
253 DakeraError::Storage("WAL writer not available after ensure_segment".to_string())
254 })?;
255
256 writer
257 .write_all(line.as_bytes())
258 .map_err(|e| DakeraError::Storage(format!("WAL write error: {}", e)))?;
259
260 let write_count = self.write_count.fetch_add(1, Ordering::Relaxed) + 1;
270 match self.config.sync_mode {
271 WalSyncMode::EveryWrite => {
272 if let Err(e) = writer.flush() {
273 tracing::warn!(error = %e, "WAL flush failed during EveryWrite sync");
274 } else if let Err(e) = writer.get_ref().sync_data() {
275 tracing::warn!(error = %e, "WAL fsync failed during EveryWrite sync");
276 }
277 }
278 WalSyncMode::Periodic(n) if n > 0 && write_count.is_multiple_of(n as u64) => {
279 if let Err(e) = writer.flush() {
280 tracing::warn!(error = %e, write_count, "WAL flush failed during periodic sync");
281 } else if let Err(e) = writer.get_ref().sync_data() {
282 tracing::warn!(error = %e, write_count, "WAL fsync failed during periodic sync");
283 }
284 }
285 _ => {}
286 }
287 }
288
289 {
291 let mut segment_guard = self.current_segment.write();
292 if let Some(ref mut segment) = *segment_guard {
293 segment.size += line.len() as u64;
294 segment.end_lsn = lsn;
295 }
296 }
297
298 let entries = self
303 .entries_since_checkpoint
304 .fetch_add(1, Ordering::Relaxed)
305 + 1;
306 if self.config.checkpoint_threshold > 0 && entries == self.config.checkpoint_threshold {
307 self.checkpoint_needed.notify_one();
308 }
309
310 Ok(lsn)
311 }
312
313 fn ensure_segment(&self) -> Result<()> {
315 let needs_new_segment = {
316 let segment_guard = self.current_segment.read();
317 match &*segment_guard {
318 None => true,
319 Some(seg) => seg.size >= self.config.max_segment_size,
320 }
321 };
322
323 if needs_new_segment {
324 self.rotate_segment()?;
325 }
326
327 Ok(())
328 }
329
330 fn rotate_segment(&self) -> Result<()> {
332 {
334 let mut writer_guard = self.writer.write();
335 if let Some(ref mut writer) = *writer_guard {
336 if let Err(e) = writer.flush() {
337 tracing::warn!(error = %e, "WAL flush failed during segment rotation");
338 }
339 }
340 *writer_guard = None;
341 }
342
343 let current_lsn = self.lsn.load(Ordering::SeqCst);
345 let segment_id = current_lsn;
346 let segment_path = self.config.wal_dir.join(format!("{:020}.wal", segment_id));
347
348 let file = OpenOptions::new()
349 .create(true)
350 .append(true)
351 .open(&segment_path)
352 .map_err(|e| DakeraError::Storage(format!("Failed to create WAL segment: {}", e)))?;
353
354 let writer = BufWriter::new(file);
355
356 {
358 let mut segment_guard = self.current_segment.write();
359 *segment_guard = Some(WalSegment {
360 size: 0,
361 start_lsn: current_lsn,
362 end_lsn: current_lsn,
363 path: segment_path.clone(),
364 });
365 }
366
367 tracing::debug!(
368 segment_id = segment_id,
369 path = %segment_path.display(),
370 "Created new WAL segment"
371 );
372
373 {
374 let mut writer_guard = self.writer.write();
375 *writer_guard = Some(writer);
376 }
377
378 Ok(())
379 }
380
381 pub fn checkpoint(&self) -> Result<u64> {
383 let lsn = self.lsn.load(Ordering::SeqCst);
384
385 self.append(WalEntry::Checkpoint { lsn })?;
387
388 self.last_checkpoint_lsn.store(lsn, Ordering::SeqCst);
390 self.entries_since_checkpoint.store(0, Ordering::SeqCst);
391
392 {
394 let mut writer_guard = self.writer.write();
395 if let Some(ref mut writer) = *writer_guard {
396 if let Err(e) = writer.flush() {
397 tracing::warn!(error = %e, lsn, "WAL flush failed during checkpoint");
398 }
399 }
400 }
401
402 Ok(lsn)
403 }
404
405 pub fn recover(&self) -> Result<Vec<WalEntry>> {
407 let segments = self.list_segments()?;
408 let checkpoint_lsn = self.last_checkpoint_lsn.load(Ordering::SeqCst);
409
410 let mut entries = Vec::new();
411
412 for segment_path in segments {
413 let segment_entries = self.read_segment(&segment_path)?;
414
415 for (lsn, entry) in segment_entries {
416 if checkpoint_lsn > 0 && lsn <= checkpoint_lsn {
418 if let WalEntry::Checkpoint { lsn: cp_lsn } = entry {
420 self.last_checkpoint_lsn.store(cp_lsn, Ordering::SeqCst);
421 }
422 continue;
423 }
424
425 match entry {
427 WalEntry::TxnBegin { .. }
428 | WalEntry::TxnCommit { .. }
429 | WalEntry::TxnRollback { .. }
430 | WalEntry::Checkpoint { .. } => continue,
431 _ => entries.push(entry),
432 }
433 }
434 }
435
436 Ok(entries)
437 }
438
439 pub fn truncate(&self, up_to_lsn: u64) -> Result<u64> {
441 let segments = self.list_segments()?;
442 let mut removed_count = 0u64;
443
444 let active_path = {
451 let segment_guard = self.current_segment.read();
452 segment_guard.as_ref().map(|s| s.path.clone())
453 };
454
455 for segment_path in segments {
456 if active_path.as_deref() == Some(segment_path.as_path()) {
458 continue;
459 }
460
461 let segment_entries = self.read_segment(&segment_path)?;
463
464 if let Some((last_lsn, _)) = segment_entries.last() {
465 if *last_lsn <= up_to_lsn {
466 fs::remove_file(&segment_path).ok();
468 removed_count += segment_entries.len() as u64;
469 }
470 }
471 }
472
473 Ok(removed_count)
474 }
475
476 pub fn current_lsn(&self) -> u64 {
478 self.lsn.load(Ordering::SeqCst)
479 }
480
481 pub fn checkpoint_needed(&self) -> &tokio::sync::Notify {
485 &self.checkpoint_needed
486 }
487
488 pub fn stats(&self) -> WalStats {
490 let segment_count = self.list_segments().map(|s| s.len()).unwrap_or(0);
491
492 let (current_segment_size, current_segment_entries) = {
493 let segment_guard = self.current_segment.read();
494 match &*segment_guard {
495 Some(seg) => (seg.size, seg.end_lsn.saturating_sub(seg.start_lsn)),
496 None => (0, 0),
497 }
498 };
499
500 WalStats {
501 current_lsn: self.lsn.load(Ordering::SeqCst),
502 last_checkpoint_lsn: self.last_checkpoint_lsn.load(Ordering::SeqCst),
503 segment_count,
504 current_segment_size,
505 current_segment_entries,
506 entries_since_checkpoint: self.entries_since_checkpoint.load(Ordering::Relaxed),
507 }
508 }
509
510 pub fn flush(&self) -> Result<()> {
512 let mut writer_guard = self.writer.write();
513 if let Some(ref mut writer) = *writer_guard {
514 writer
515 .flush()
516 .map_err(|e| DakeraError::Storage(format!("WAL flush error: {}", e)))?;
517 }
518 Ok(())
519 }
520}
521
522#[derive(Debug, Clone)]
524pub struct WalStats {
525 pub current_lsn: u64,
527 pub last_checkpoint_lsn: u64,
529 pub segment_count: usize,
531 pub current_segment_size: u64,
533 pub current_segment_entries: u64,
535 pub entries_since_checkpoint: u64,
537}
538
539pub struct WalStorage<S> {
541 inner: S,
543 wal: WriteAheadLog,
545 snapshot_mgr: Option<SnapshotManager>,
548 write_gate: tokio::sync::RwLock<()>,
553}
554
555impl<S> WalStorage<S> {
556 pub fn new(inner: S, wal_config: WalConfig) -> Result<Self> {
558 let wal = WriteAheadLog::new(wal_config)?;
559 Ok(Self {
560 inner,
561 wal,
562 snapshot_mgr: None,
563 write_gate: tokio::sync::RwLock::new(()),
564 })
565 }
566
567 pub fn with_snapshots(mut self, snapshot_config: SnapshotConfig) -> Result<Self> {
571 self.snapshot_mgr = Some(SnapshotManager::new(snapshot_config)?);
572 Ok(self)
573 }
574
575 pub fn wal(&self) -> &WriteAheadLog {
577 &self.wal
578 }
579
580 pub fn inner(&self) -> &S {
582 &self.inner
583 }
584
585 pub fn checkpoint(&self) -> Result<u64> {
587 self.wal.checkpoint()
588 }
589}
590
591impl<S: VectorStorage> WalStorage<S> {
592 pub async fn recover_into_inner(&self) -> Result<usize> {
597 if let Some(mgr) = &self.snapshot_mgr {
601 if let Some(latest) = mgr
602 .list_snapshots()?
603 .into_iter()
604 .max_by_key(|m| m.created_at)
605 {
606 let restored = mgr.restore_snapshot(&self.inner, &latest.id).await?;
607 tracing::info!(
608 snapshot_id = %latest.id,
609 namespaces = restored.namespaces_restored,
610 vectors = restored.vectors_restored,
611 "WAL recovery restored base snapshot"
612 );
613 }
614 }
615
616 let entries = self.wal.recover()?;
617 let mut applied = 0usize;
618 for entry in entries {
619 match entry {
620 WalEntry::CreateNamespace { namespace } => {
621 self.inner.ensure_namespace(&namespace).await?;
622 }
623 WalEntry::Upsert { namespace, vectors } => {
624 let vecs: Vec<Vector> = vectors
625 .into_iter()
626 .map(SerializedVector::into_vector)
627 .collect();
628 if !vecs.is_empty() {
629 self.inner.upsert(&namespace, vecs).await?;
630 }
631 }
632 WalEntry::Delete { namespace, ids } => {
633 self.inner.delete(&namespace, &ids).await?;
634 }
635 WalEntry::DeleteNamespace { namespace } => {
636 self.inner.delete_namespace(&namespace).await?;
637 }
638 WalEntry::Checkpoint { .. }
640 | WalEntry::TxnBegin { .. }
641 | WalEntry::TxnCommit { .. }
642 | WalEntry::TxnRollback { .. } => continue,
643 }
644 applied += 1;
645 }
646 if applied > 0 {
647 tracing::info!(
648 entries = applied,
649 "WAL recovery replayed mutations into inner storage"
650 );
651 }
652 Ok(applied)
653 }
654
655 pub async fn snapshot_and_checkpoint(&self) -> Result<Option<u64>> {
662 let Some(mgr) = &self.snapshot_mgr else {
663 return Ok(None);
664 };
665 let _gate = self.write_gate.write().await;
667 let lsn = self.wal.current_lsn();
668 mgr.create_snapshot(&self.inner, Some(format!("wal-checkpoint-{lsn}")))
669 .await?;
670 self.wal.checkpoint()?;
671 let removed = self.wal.truncate(lsn)?;
672 tracing::info!(
673 lsn,
674 segments_removed = removed,
675 "WAL snapshot + checkpoint + truncate complete"
676 );
677 Ok(Some(lsn))
678 }
679}
680
681#[async_trait::async_trait]
682impl<S: VectorStorage> VectorStorage for WalStorage<S> {
683 async fn upsert(&self, namespace: &NamespaceId, vectors: Vec<Vector>) -> Result<usize> {
684 let _gate = self.write_gate.read().await;
687 let serialized: Vec<SerializedVector> =
690 vectors.iter().map(SerializedVector::from_vector).collect();
691 self.wal.append(WalEntry::Upsert {
692 namespace: namespace.clone(),
693 vectors: serialized,
694 })?;
695 self.inner.upsert(namespace, vectors).await
696 }
697
698 async fn get(&self, namespace: &NamespaceId, ids: &[VectorId]) -> Result<Vec<Vector>> {
699 self.inner.get(namespace, ids).await
700 }
701
702 async fn get_all(&self, namespace: &NamespaceId) -> Result<Vec<Vector>> {
703 self.inner.get_all(namespace).await
704 }
705
706 async fn get_all_meta(&self, namespace: &NamespaceId) -> Result<Vec<Vector>> {
707 self.inner.get_all_meta(namespace).await
708 }
709
710 async fn delete(&self, namespace: &NamespaceId, ids: &[VectorId]) -> Result<usize> {
711 let _gate = self.write_gate.read().await;
712 self.wal.append(WalEntry::Delete {
713 namespace: namespace.clone(),
714 ids: ids.to_vec(),
715 })?;
716 self.inner.delete(namespace, ids).await
717 }
718
719 async fn namespace_exists(&self, namespace: &NamespaceId) -> Result<bool> {
720 self.inner.namespace_exists(namespace).await
721 }
722
723 async fn ensure_namespace(&self, namespace: &NamespaceId) -> Result<()> {
724 let _gate = self.write_gate.read().await;
725 self.wal.append(WalEntry::CreateNamespace {
726 namespace: namespace.clone(),
727 })?;
728 self.inner.ensure_namespace(namespace).await
729 }
730
731 async fn count(&self, namespace: &NamespaceId) -> Result<usize> {
732 self.inner.count(namespace).await
733 }
734
735 async fn dimension(&self, namespace: &NamespaceId) -> Result<Option<usize>> {
736 self.inner.dimension(namespace).await
737 }
738
739 async fn list_namespaces(&self) -> Result<Vec<NamespaceId>> {
740 self.inner.list_namespaces().await
741 }
742
743 async fn delete_namespace(&self, namespace: &NamespaceId) -> Result<bool> {
744 let _gate = self.write_gate.read().await;
745 self.wal.append(WalEntry::DeleteNamespace {
746 namespace: namespace.clone(),
747 })?;
748 self.inner.delete_namespace(namespace).await
749 }
750
751 async fn cleanup_expired(&self, namespace: &NamespaceId) -> Result<usize> {
752 self.inner.cleanup_expired(namespace).await
755 }
756
757 async fn cleanup_all_expired(&self) -> Result<usize> {
758 self.inner.cleanup_all_expired().await
759 }
760
761 async fn get_page(
762 &self,
763 namespace: &NamespaceId,
764 offset: usize,
765 limit: usize,
766 ) -> Result<Vec<Vector>> {
767 self.inner.get_page(namespace, offset, limit).await
768 }
769
770 async fn reclaim_derived_caches(&self) {
771 self.inner.reclaim_derived_caches().await;
772 }
773}
774
775#[cfg(test)]
776mod tests {
777 use super::*;
778 use tempfile::TempDir;
779
780 fn test_config(dir: &Path) -> WalConfig {
781 WalConfig {
782 wal_dir: dir.to_path_buf(),
783 max_segment_size: 1024,
784 sync_mode: WalSyncMode::EveryWrite,
785 checkpoint_threshold: 100,
786 }
787 }
788
789 fn tv(id: &str) -> Vector {
790 Vector {
791 id: id.to_string(),
792 values: vec![0.1, 0.2, 0.3],
793 metadata: None,
794 ttl_seconds: None,
795 expires_at: None,
796 }
797 }
798
799 #[tokio::test]
803 async fn wal_snapshot_checkpoint_recovers_all_data() {
804 use crate::InMemoryStorage;
805 let wal_dir = TempDir::new().unwrap();
806 let snap_dir = TempDir::new().unwrap();
807 let ns = "ns1".to_string();
808 let wal_cfg = test_config(wal_dir.path());
809 let snap_cfg = SnapshotConfig {
810 snapshot_dir: snap_dir.path().to_path_buf(),
811 ..Default::default()
812 };
813
814 {
816 let store = WalStorage::new(InMemoryStorage::new(), wal_cfg.clone())
817 .unwrap()
818 .with_snapshots(snap_cfg.clone())
819 .unwrap();
820 store.ensure_namespace(&ns).await.unwrap();
821 store.upsert(&ns, vec![tv("a"), tv("b")]).await.unwrap();
822 store.snapshot_and_checkpoint().await.unwrap();
823 store.upsert(&ns, vec![tv("c")]).await.unwrap();
824 store.delete(&ns, &["a".to_string()]).await.unwrap();
825 }
826
827 let store2 = WalStorage::new(InMemoryStorage::new(), wal_cfg)
829 .unwrap()
830 .with_snapshots(snap_cfg)
831 .unwrap();
832 store2.recover_into_inner().await.unwrap();
833
834 let ids: std::collections::HashSet<String> = store2
835 .get_all(&ns)
836 .await
837 .unwrap()
838 .into_iter()
839 .map(|v| v.id)
840 .collect();
841 assert!(ids.contains("b"), "b (from snapshot) must survive: {ids:?}");
842 assert!(ids.contains("c"), "c (from WAL tail) must survive: {ids:?}");
843 assert!(
844 !ids.contains("a"),
845 "a (deleted post-checkpoint) must stay deleted"
846 );
847 }
848
849 #[tokio::test]
852 async fn checkpoint_threshold_signals_driver() {
853 let dir = TempDir::new().unwrap();
854 let mut cfg = test_config(dir.path());
855 cfg.checkpoint_threshold = 3;
856 let wal = WriteAheadLog::new(cfg).unwrap();
857 let entry = || WalEntry::Upsert {
858 namespace: "n".to_string(),
859 vectors: vec![],
860 };
861 wal.append(entry()).unwrap();
863 wal.append(entry()).unwrap();
864 assert!(
865 tokio::time::timeout(
866 std::time::Duration::from_millis(50),
867 wal.checkpoint_needed().notified()
868 )
869 .await
870 .is_err(),
871 "must not signal before the threshold is reached"
872 );
873 wal.append(entry()).unwrap();
875 tokio::time::timeout(
876 std::time::Duration::from_millis(100),
877 wal.checkpoint_needed().notified(),
878 )
879 .await
880 .expect("crossing checkpoint_threshold must signal the driver");
881 }
882
883 #[test]
884 fn test_wal_basic_operations() {
885 let temp_dir = TempDir::new().unwrap();
886 let config = test_config(temp_dir.path());
887 let wal = WriteAheadLog::new(config).unwrap();
888
889 let lsn1 = wal
891 .append(WalEntry::CreateNamespace {
892 namespace: "test".to_string(),
893 })
894 .unwrap();
895
896 let lsn2 = wal
897 .append(WalEntry::Upsert {
898 namespace: "test".to_string(),
899 vectors: vec![SerializedVector {
900 id: "v1".to_string(),
901 values: vec![1.0, 2.0, 3.0],
902 metadata: None,
903 ttl_seconds: None,
904 expires_at: None,
905 }],
906 })
907 .unwrap();
908
909 assert_eq!(lsn1, 0);
910 assert_eq!(lsn2, 1);
911 assert_eq!(wal.current_lsn(), 2);
912 }
913
914 #[test]
915 fn test_wal_recovery() {
916 let temp_dir = TempDir::new().unwrap();
917 let config = test_config(temp_dir.path());
918
919 {
921 let wal = WriteAheadLog::new(config.clone()).unwrap();
922 wal.append(WalEntry::CreateNamespace {
923 namespace: "test".to_string(),
924 })
925 .unwrap();
926 wal.append(WalEntry::Upsert {
927 namespace: "test".to_string(),
928 vectors: vec![SerializedVector {
929 id: "v1".to_string(),
930 values: vec![1.0, 2.0],
931 metadata: None,
932 ttl_seconds: None,
933 expires_at: None,
934 }],
935 })
936 .unwrap();
937 wal.flush().unwrap();
938 }
939
940 {
942 let wal = WriteAheadLog::new(config).unwrap();
943 let entries = wal.recover().unwrap();
944
945 assert_eq!(entries.len(), 2);
946 assert!(matches!(entries[0], WalEntry::CreateNamespace { .. }));
947 assert!(matches!(entries[1], WalEntry::Upsert { .. }));
948 }
949 }
950
951 #[test]
952 fn test_wal_checkpoint() {
953 let temp_dir = TempDir::new().unwrap();
954 let config = test_config(temp_dir.path());
955 let wal = WriteAheadLog::new(config).unwrap();
956
957 wal.append(WalEntry::CreateNamespace {
959 namespace: "test".to_string(),
960 })
961 .unwrap();
962 let _checkpoint_lsn = wal.checkpoint().unwrap();
963
964 wal.append(WalEntry::Upsert {
965 namespace: "test".to_string(),
966 vectors: vec![],
967 })
968 .unwrap();
969
970 let stats = wal.stats();
971 assert!(stats.last_checkpoint_lsn > 0);
972 assert_eq!(stats.entries_since_checkpoint, 1); }
974
975 #[test]
976 fn test_wal_stats() {
977 let temp_dir = TempDir::new().unwrap();
978 let config = test_config(temp_dir.path());
979 let wal = WriteAheadLog::new(config).unwrap();
980
981 for i in 0..5 {
982 wal.append(WalEntry::Upsert {
983 namespace: "test".to_string(),
984 vectors: vec![SerializedVector {
985 id: format!("v{}", i),
986 values: vec![i as f32],
987 metadata: None,
988 ttl_seconds: None,
989 expires_at: None,
990 }],
991 })
992 .unwrap();
993 }
994
995 let stats = wal.stats();
996 assert_eq!(stats.current_lsn, 5);
997 assert_eq!(stats.entries_since_checkpoint, 5);
998 }
999
1000 #[test]
1001 fn test_segment_rotation() {
1002 let temp_dir = TempDir::new().unwrap();
1003 let config = WalConfig {
1004 wal_dir: temp_dir.path().to_path_buf(),
1005 max_segment_size: 100, sync_mode: WalSyncMode::EveryWrite,
1007 checkpoint_threshold: 1000,
1008 };
1009
1010 let wal = WriteAheadLog::new(config).unwrap();
1011
1012 for i in 0..10 {
1014 wal.append(WalEntry::Upsert {
1015 namespace: "test".to_string(),
1016 vectors: vec![SerializedVector {
1017 id: format!("v{}", i),
1018 values: vec![i as f32; 10],
1019 metadata: Some("some metadata here".to_string()),
1020 ttl_seconds: None,
1021 expires_at: None,
1022 }],
1023 })
1024 .unwrap();
1025 }
1026
1027 let stats = wal.stats();
1028 assert!(stats.segment_count > 1);
1029 }
1030}