#![allow( clippy::too_many_lines, clippy::cast_precision_loss, clippy::cast_sign_loss, clippy::missing_const_for_fn )] use std::{sync::Arc, time::Duration}; use tidaldb::wal::{SignalEvent, WalConfig, WalHandle, checkpoint::CheckpointManager}; fn test_config(dir: &std::path::Path) -> WalConfig { WalConfig { dir: dir.to_path_buf(), segment_size: 16 * 1024 * 1024, batch_size: 100, batch_timeout: Duration::from_millis(1), dedup_window: Duration::from_secs(30), } } fn make_event(id: u64) -> SignalEvent { SignalEvent { entity_id: id, signal_type: 1, weight: 1.0, timestamp_nanos: id * 1_000_000_000, } } // -- AC-17, AC-18: Checkpoint and truncation #[test] fn wal_checkpoint_and_truncation() { let dir = tempfile::tempdir().expect("tempdir creation should succeed"); // Small segments so we get multiple let config = WalConfig { dir: dir.path().to_path_buf(), segment_size: 256, batch_size: 5, batch_timeout: Duration::from_millis(10), dedup_window: Duration::from_secs(30), }; let (handle, _, _) = WalHandle::open(config).expect("open should succeed"); // Write events let mut last_seq = 0; for i in 1..=50 { let seq = handle.append(make_event(i)).expect("append should succeed"); if seq > last_seq { last_seq = seq; } } // Checkpoint at a mid-point let checkpoint_seq = last_seq / 2; handle .checkpoint(checkpoint_seq) .expect("checkpoint should succeed"); // Verify checkpoint file exists and is correct let wal_dir = dir.path().join("wal"); let cp = CheckpointManager::read(&wal_dir).expect("read should succeed"); let (seq, _ts) = cp.expect("checkpoint should exist"); assert_eq!(seq, checkpoint_seq); // Truncate segments before checkpoint handle .truncate_before(checkpoint_seq) .expect("truncate should succeed"); handle.shutdown().expect("shutdown should succeed"); // Reopen and verify: only events >= checkpoint_seq are replayed let config = WalConfig { dir: dir.path().to_path_buf(), segment_size: 256, batch_size: 5, batch_timeout: Duration::from_millis(10), dedup_window: Duration::from_secs(30), }; let (handle, replayed, _) = WalHandle::open(config).expect("reopen should succeed"); assert!( !replayed.is_empty(), "should replay events after checkpoint" ); // All replayed events should have sequence >= checkpoint_seq // (we verify this implicitly by checking count) handle.shutdown().expect("shutdown should succeed"); } // -- AC-19: Concurrent writers #[test] fn wal_concurrent_writers() { let dir = tempfile::tempdir().expect("tempdir creation should succeed"); let config = test_config(dir.path()); let (handle, _, _) = WalHandle::open(config).expect("open should succeed"); let handle = Arc::new(handle); let num_threads = 8; let events_per_thread = 100; let mut threads = Vec::new(); for thread_id in 0..num_threads { let handle = Arc::clone(&handle); threads.push(std::thread::spawn(move || { let mut seqs = Vec::with_capacity(events_per_thread); for i in 0..events_per_thread { // Each thread uses unique entity_ids to avoid dedup let entity_id = thread_id as u64 * events_per_thread as u64 + i as u64; let event = SignalEvent { entity_id, signal_type: thread_id as u8, weight: 1.0, timestamp_nanos: entity_id * 1_000, }; let seq = handle.append(event).expect("append should succeed"); seqs.push(seq); } seqs })); } let mut all_seqs = Vec::new(); for thread in threads { let seqs = thread.join().expect("thread should join"); all_seqs.extend(seqs); } // Shutdown by unwrapping the Arc (only holder now) let handle = Arc::try_unwrap(handle).expect("should be sole owner of WalHandle Arc"); handle.shutdown().expect("shutdown should succeed"); // Filter out dedup seq=0 (should be none) let non_zero: Vec = all_seqs.iter().copied().filter(|&s| s > 0).collect(); assert_eq!( non_zero.len(), num_threads * events_per_thread, "all {} events should get unique sequence numbers", num_threads * events_per_thread ); // No duplicate sequence numbers let mut sorted = non_zero.clone(); sorted.sort_unstable(); sorted.dedup(); assert_eq!( sorted.len(), non_zero.len(), "no duplicate sequence numbers allowed" ); // Verify all checksums valid on replay let config = test_config(dir.path()); let (handle, replayed, _) = WalHandle::open(config).expect("reopen should succeed"); assert_eq!( replayed.len(), num_threads * events_per_thread, "all events should be present on replay" ); handle.shutdown().expect("shutdown should succeed"); } // -- AC-4: Sequence numbers survive close/reopen #[test] fn wal_close_and_reopen() { let dir = tempfile::tempdir().expect("tempdir creation should succeed"); let mut last_seq = 0; // Session 1: write 10 events let config = test_config(dir.path()); let (handle, _, _) = WalHandle::open(config).expect("open should succeed"); for i in 1..=10 { let seq = handle.append(make_event(i)).expect("append should succeed"); if seq > last_seq { last_seq = seq; } } handle.shutdown().expect("shutdown should succeed"); // Session 2: write 10 more, verify seqs continue let config = test_config(dir.path()); let (handle, replayed, _) = WalHandle::open(config).expect("reopen should succeed"); assert_eq!(replayed.len(), 10); for i in 11..=20 { let seq = handle.append(make_event(i)).expect("append should succeed"); assert!(seq > last_seq, "seq {seq} should be > last_seq {last_seq}"); last_seq = seq; } handle.shutdown().expect("shutdown should succeed"); // Session 3: verify all 20 events let config = test_config(dir.path()); let (handle, replayed, _) = WalHandle::open(config).expect("reopen should succeed"); assert_eq!(replayed.len(), 20); handle.shutdown().expect("shutdown should succeed"); } #[test] fn wal_replay_correctness() { let dir = tempfile::tempdir().expect("tempdir creation should succeed"); let config = test_config(dir.path()); // Write 100 events let (handle, _, _) = WalHandle::open(config).expect("open should succeed"); let mut seqs = Vec::new(); for i in 1..=100 { let seq = handle.append(make_event(i)).expect("append should succeed"); seqs.push(seq); } // Checkpoint at event 50 let checkpoint_seq = seqs[49]; // seq of the 50th event handle .checkpoint(checkpoint_seq) .expect("checkpoint should succeed"); handle.shutdown().expect("shutdown should succeed"); // Reopen and verify: only post-checkpoint events are replayed let config = test_config(dir.path()); let (handle, replayed, _) = WalHandle::open(config).expect("reopen should succeed"); // Events with seq >= checkpoint_seq should be replayed. // The exact count depends on batching, but it should be at least 50 // (the events after the checkpoint) and at most 100. assert!( replayed.len() >= 50, "expected at least 50 replayed events, got {}", replayed.len() ); assert!( replayed.len() <= 100, "expected at most 100 replayed events, got {}", replayed.len() ); handle.shutdown().expect("shutdown should succeed"); }