- 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
204 lines
6.5 KiB
Rust
204 lines
6.5 KiB
Rust
#![allow(
|
|
clippy::cast_precision_loss,
|
|
clippy::cast_sign_loss,
|
|
clippy::missing_const_for_fn
|
|
)]
|
|
|
|
use std::{sync::Arc, time::Duration};
|
|
|
|
use tidaldb::{
|
|
replication::ShardId,
|
|
wal::{SignalEvent, WalConfig, WalHandle, 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-1, AC-2: Wire format byte-level tests are in format.rs unit tests.
|
|
// These integration tests validate the full pipeline.
|
|
|
|
#[test]
|
|
fn wal_basic_round_trip() {
|
|
let dir = tempfile::tempdir().expect("tempdir creation should succeed");
|
|
let config = test_config(dir.path());
|
|
|
|
// Write events
|
|
let (handle, replayed, _) = WalHandle::open(config).expect("open should succeed");
|
|
assert!(replayed.is_empty());
|
|
|
|
for i in 1..=10 {
|
|
handle.append(make_event(i)).expect("append should succeed");
|
|
}
|
|
handle.shutdown().expect("shutdown should succeed");
|
|
|
|
// Reopen and verify replay
|
|
let config = test_config(dir.path());
|
|
let (handle, replayed, _) = WalHandle::open(config).expect("reopen should succeed");
|
|
assert_eq!(replayed.len(), 10);
|
|
for (i, event) in replayed.iter().enumerate() {
|
|
assert_eq!(event.entity_id, (i + 1) as u64);
|
|
assert_eq!(event.signal_type, 1);
|
|
assert_eq!(event.weight.to_bits(), 1.0_f32.to_bits());
|
|
}
|
|
handle.shutdown().expect("shutdown should succeed");
|
|
}
|
|
|
|
// -- AC-10, AC-11: Deduplication
|
|
#[test]
|
|
fn wal_dedup_silent() {
|
|
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 event = make_event(42);
|
|
let seq1 = handle
|
|
.append(event.clone())
|
|
.expect("first append should succeed");
|
|
let seq2 = handle
|
|
.append(event.clone())
|
|
.expect("second append should succeed");
|
|
let seq3 = handle.append(event).expect("third append should succeed");
|
|
|
|
assert!(seq1 > 0, "first event should get real sequence number");
|
|
assert_eq!(seq2, 0, "duplicate should return seq=0");
|
|
assert_eq!(seq3, 0, "duplicate should return seq=0");
|
|
|
|
handle.shutdown().expect("shutdown should succeed");
|
|
|
|
// Verify only one event on disk
|
|
let config = test_config(dir.path());
|
|
let (handle, replayed, _) = WalHandle::open(config).expect("reopen should succeed");
|
|
assert_eq!(replayed.len(), 1, "only one unique event should be on disk");
|
|
handle.shutdown().expect("shutdown should succeed");
|
|
}
|
|
|
|
// -- AC-12: No false positives
|
|
#[test]
|
|
fn wal_dedup_no_false_positives() {
|
|
let dir = tempfile::tempdir().expect("tempdir creation should succeed");
|
|
// Use a large batch size so batches fill quickly from concurrent writers.
|
|
let config = WalConfig {
|
|
dir: dir.path().to_path_buf(),
|
|
segment_size: 16 * 1024 * 1024,
|
|
batch_size: 256,
|
|
batch_timeout: Duration::from_millis(1),
|
|
dedup_window: Duration::from_secs(60),
|
|
};
|
|
|
|
let (handle, _, _) = WalHandle::open(config).expect("open should succeed");
|
|
let handle = Arc::new(handle);
|
|
|
|
let total_events: u64 = 1_000;
|
|
let num_threads = 10u64;
|
|
let per_thread = total_events / num_threads;
|
|
|
|
let mut threads = Vec::new();
|
|
for t in 0..num_threads {
|
|
let handle = Arc::clone(&handle);
|
|
threads.push(std::thread::spawn(move || {
|
|
let mut count = 0u64;
|
|
for i in 0..per_thread {
|
|
let entity_id = t * per_thread + i;
|
|
let event = SignalEvent {
|
|
entity_id,
|
|
signal_type: (entity_id % 256) as u8,
|
|
weight: entity_id as f32,
|
|
timestamp_nanos: entity_id * 1_000_000,
|
|
};
|
|
let seq = handle.append(event).expect("append should succeed");
|
|
if seq > 0 {
|
|
count += 1;
|
|
}
|
|
}
|
|
count
|
|
}));
|
|
}
|
|
|
|
let mut real_seqs = 0u64;
|
|
for thread in threads {
|
|
real_seqs += thread.join().expect("thread should join");
|
|
}
|
|
|
|
let handle = Arc::try_unwrap(handle).expect("should be sole owner of WalHandle Arc");
|
|
handle.shutdown().expect("shutdown should succeed");
|
|
|
|
assert_eq!(
|
|
real_seqs, total_events,
|
|
"all {total_events} unique events must be accepted (no false positives)"
|
|
);
|
|
}
|
|
|
|
// -- AC-5, AC-6: Segment rotation
|
|
#[test]
|
|
fn wal_segment_rotation() {
|
|
let dir = tempfile::tempdir().expect("tempdir creation should succeed");
|
|
// Use very small segment size to force rotation
|
|
let config = WalConfig {
|
|
dir: dir.path().to_path_buf(),
|
|
segment_size: 256, // tiny: one batch exceeds this
|
|
batch_size: 10,
|
|
batch_timeout: Duration::from_millis(10),
|
|
dedup_window: Duration::from_secs(30),
|
|
};
|
|
|
|
let (handle, _, _) = WalHandle::open(config).expect("open should succeed");
|
|
|
|
// Write enough events to trigger multiple rotations
|
|
for i in 1..=100 {
|
|
handle.append(make_event(i)).expect("append should succeed");
|
|
}
|
|
handle.shutdown().expect("shutdown should succeed");
|
|
|
|
// Check segment files exist
|
|
let wal_dir = dir.path().join("wal");
|
|
let segments = segment::list_segments(&wal_dir).expect("list should succeed");
|
|
assert!(
|
|
segments.len() > 1,
|
|
"expected multiple segments, got {}",
|
|
segments.len()
|
|
);
|
|
|
|
// Verify segment naming: all should match wal-{seq:020}.seg pattern
|
|
for (seq, path) in &segments {
|
|
let filename = path
|
|
.file_name()
|
|
.expect("should have filename")
|
|
.to_str()
|
|
.expect("should be valid UTF-8");
|
|
assert_eq!(
|
|
filename,
|
|
segment::segment_filename(ShardId::SINGLE, *seq),
|
|
"segment filename mismatch"
|
|
);
|
|
}
|
|
|
|
// Verify replay gets all events
|
|
let config = WalConfig {
|
|
dir: dir.path().to_path_buf(),
|
|
segment_size: 256,
|
|
batch_size: 10,
|
|
batch_timeout: Duration::from_millis(10),
|
|
dedup_window: Duration::from_secs(30),
|
|
};
|
|
let (handle, replayed, _) = WalHandle::open(config).expect("reopen should succeed");
|
|
assert_eq!(replayed.len(), 100, "all events should be replayed");
|
|
handle.shutdown().expect("shutdown should succeed");
|
|
}
|