tidaldb/tidal/tests/wal_integration_crash.rs
jx12n 3bcfb3c576 feat: Bazel build, crate docs/ai-lookup, docker images, and engine hardening
- Add BUILD.bazel across tidal, tidal-net, tidal-server, tidalctl for bzlmod build
- Add tidal/ crate docs (README, CHANGELOG, CONTRIBUTING, AGENTS, CLAUDE, API, ARCHITECTURE) and ai-lookup reference
- Add docker standalone/cluster/deploy images, compose, and prometheus config
- Harden WAL (batch format, writer, dedup, diagnostics), text syncer/collectors, and vector registry
- Expand tidalctl CLI and tests; restructure WAL/visibility integration test suites
- Refine tidal-net transport/client/server and tidal-server cluster/scatter-gather
2026-06-07 18:29:38 -06:00

159 lines
4.9 KiB
Rust

#![allow(
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 {
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");
}
}