- 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)
245 lines
7.9 KiB
Rust
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");
|
|
}
|