tidaldb/tidal/tests/wal_integration_checkpoint.rs
jx12n ad4134e280 chore: doc consolidation, seven-dimension review fixes, and commit hooks
- Eliminate the tidal/ self-contained doc mirror; docs now have two canonical
  homes (root *.md and docs/), with planning/specs/research/reviews moved up
- Remove stale .agents/skills and .ai mirrors; canonicalize skills under .claude/
- Add pre-commit hook + scripts/check-docs.sh doc-guard + scripts/install-hooks.sh
- Implement M0-M10 seven-dimension review findings across engine, net, server,
  and tidalctl (durability, replication, query, WAL, storage, CLI hardening)
2026-06-08 22:46:28 -06:00

245 lines
7.9 KiB
Rust

#![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 {
wal_dir_override: None,
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 {
wal_dir_override: None,
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 {
wal_dir_override: None,
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<u64> = 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");
}