tidaldb/tidal/tests/wal_integration_crash.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

161 lines
5.0 KiB
Rust

#![allow(
clippy::too_many_lines,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::missing_const_for_fn
)]
use std::{fs, time::Duration};
use tidaldb::{
replication::ShardId,
wal::{
SignalEvent, WalConfig, WalHandle,
format::{self, EventRecord, HEADER_SIZE},
reader, segment,
},
};
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-13, AC-14: Crash recovery with torn write
#[test]
fn wal_crash_recovery_torn_write() {
let dir = tempfile::tempdir().expect("tempdir creation should succeed");
let wal_dir = dir.path().join("wal");
fs::create_dir_all(&wal_dir).expect("create dir should succeed");
// Write valid batches directly to simulate a crash mid-write
let events1: Vec<EventRecord> = (1..=5)
.map(|i| EventRecord::signal(i, 1, 1.0, i * 1_000_000_000))
.collect();
let events2: Vec<EventRecord> = (6..=10)
.map(|i| EventRecord::signal(i, 1, 1.0, i * 1_000_000_000))
.collect();
let batch1 = format::encode_batch(&events1, 1, 1_000_000_000).expect("encode should succeed");
let batch2 = format::encode_batch(&events2, 6, 6_000_000_000).expect("encode should succeed");
// Write batch1 fully, then truncate batch2 at various offsets
for truncate_at in [
0,
10,
32,
63,
HEADER_SIZE,
HEADER_SIZE + 5,
HEADER_SIZE + 20,
] {
let seg_name = segment::segment_filename(ShardId::SINGLE, 1);
let seg_path = wal_dir.join(&seg_name);
let mut data = batch1.clone();
if truncate_at > 0 {
data.extend_from_slice(&batch2[..truncate_at.min(batch2.len())]);
}
fs::write(&seg_path, &data).expect("write should succeed");
let recovery = reader::recover(&wal_dir).expect("recovery should succeed");
assert_eq!(
recovery.events.len(),
5,
"torn write at offset {truncate_at}: should recover 5 events"
);
// Clean up for next iteration
fs::remove_file(&seg_path).expect("cleanup should succeed");
}
}
// -- AC-15: No phantom records (clean shutdown variant)
#[test]
fn wal_clean_shutdown_no_data_loss() {
let dir = tempfile::tempdir().expect("tempdir creation should succeed");
let config = test_config(dir.path());
// Write 5 events
let (handle, _, _) = WalHandle::open(config).expect("open should succeed");
for i in 1..=5 {
handle.append(make_event(i)).expect("append should succeed");
}
handle.shutdown().expect("shutdown should succeed");
// Verify exactly 5 events on replay
let config = test_config(dir.path());
let (handle, replayed, _) = WalHandle::open(config).expect("reopen should succeed");
assert_eq!(
replayed.len(),
5,
"should replay exactly 5 events, not more"
);
// No phantom events (events from un-fsynced batches should not appear)
for event in &replayed {
assert!(
event.entity_id >= 1 && event.entity_id <= 5,
"unexpected entity_id {}",
event.entity_id
);
}
handle.shutdown().expect("shutdown should succeed");
}
// -- AC-16: Crash at any byte position never produces corrupt state
#[test]
fn wal_crash_at_any_byte_position() {
let dir = tempfile::tempdir().expect("tempdir creation should succeed");
let wal_dir = dir.path().join("wal");
fs::create_dir_all(&wal_dir).expect("create dir should succeed");
let events: Vec<EventRecord> = (1..=3)
.map(|i| EventRecord::signal(i, 1, 1.0, i * 1_000_000_000))
.collect();
let batch = format::encode_batch(&events, 1, 1_000_000_000).expect("encode should succeed");
// Test truncation at every byte offset
for truncate_at in 0..=batch.len() {
let seg_name = segment::segment_filename(ShardId::SINGLE, 1);
let seg_path = wal_dir.join(&seg_name);
fs::write(&seg_path, &batch[..truncate_at]).expect("write should succeed");
let recovery = reader::recover(&wal_dir).expect("recovery should never fail");
if truncate_at == batch.len() {
assert_eq!(
recovery.events.len(),
3,
"full batch should recover 3 events"
);
} else {
assert_eq!(
recovery.events.len(),
0,
"truncated at byte {truncate_at}: no events should be recovered"
);
}
// Clean up for next iteration
fs::remove_file(&seg_path).expect("cleanup should succeed");
}
}