allsource_core/infrastructure/persistence/
wal.rs1use crate::{
2 domain::entities::Event,
3 error::{AllSourceError, Result},
4};
5use chrono::{DateTime, Utc};
6use parking_lot::RwLock;
7use serde::{Deserialize, Serialize};
8use std::{
9 fs::{self, File, OpenOptions},
10 io::{BufRead, BufReader, BufWriter, Write},
11 path::{Path, PathBuf},
12 sync::Arc,
13};
14
15pub struct WriteAheadLog {
17 wal_dir: PathBuf,
19
20 current_file: Arc<RwLock<WALFile>>,
22
23 config: WALConfig,
25
26 stats: Arc<RwLock<WALStats>>,
28
29 sequence: Arc<RwLock<u64>>,
31
32 replication_tx: parking_lot::Mutex<Option<tokio::sync::broadcast::Sender<WALEntry>>>,
36}
37
38#[derive(Debug, Clone)]
39pub struct WALConfig {
40 pub max_file_size: usize,
42
43 pub sync_on_write: bool,
45
46 pub max_wal_files: usize,
48
49 pub compress: bool,
51
52 pub fsync_interval_ms: Option<u64>,
57}
58
59impl Default for WALConfig {
60 fn default() -> Self {
61 Self {
62 max_file_size: 64 * 1024 * 1024, sync_on_write: true,
64 max_wal_files: 10,
65 compress: false,
66 fsync_interval_ms: None,
67 }
68 }
69}
70
71#[derive(Debug, Clone, Default, Serialize)]
72pub struct WALStats {
73 pub total_entries: u64,
74 pub total_bytes_written: u64,
75 pub current_file_size: usize,
76 pub files_rotated: u64,
77 pub files_cleaned: u64,
78 pub recovery_count: u64,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct WALEntry {
84 pub sequence: u64,
86
87 pub wal_timestamp: DateTime<Utc>,
89
90 pub event: Event,
92
93 pub checksum: u32,
95}
96
97impl WALEntry {
98 pub fn new(sequence: u64, event: Event) -> Self {
99 let mut entry = Self {
100 sequence,
101 wal_timestamp: Utc::now(),
102 event,
103 checksum: 0,
104 };
105 entry.checksum = entry.calculate_checksum();
106 entry
107 }
108
109 fn calculate_checksum(&self) -> u32 {
110 let data = format!("{}{}{}", self.sequence, self.wal_timestamp, self.event.id);
112 crc32fast::hash(data.as_bytes())
113 }
114
115 pub fn verify(&self) -> bool {
116 self.checksum == self.calculate_checksum()
117 }
118}
119
120struct WALFile {
122 path: PathBuf,
123 writer: BufWriter<File>,
124 size: usize,
125 created_at: DateTime<Utc>,
126}
127
128impl WALFile {
129 fn new(path: PathBuf) -> Result<Self> {
130 let file = OpenOptions::new()
131 .create(true)
132 .append(true)
133 .open(&path)
134 .map_err(|e| AllSourceError::StorageError(format!("Failed to open WAL file: {e}")))?;
135
136 let size = file.metadata().map(|m| m.len() as usize).unwrap_or(0);
137
138 Ok(Self {
139 path,
140 writer: BufWriter::new(file),
141 size,
142 created_at: Utc::now(),
143 })
144 }
145
146 fn write_entry(&mut self, entry: &WALEntry, sync: bool) -> Result<usize> {
147 let json = serde_json::to_string(entry)?;
149
150 let line = format!("{}\n", json);
151 let bytes_written = line.len();
152
153 self.writer
154 .write_all(line.as_bytes())
155 .map_err(|e| AllSourceError::StorageError(format!("Failed to write to WAL: {e}")))?;
156
157 if sync {
158 self.writer
159 .flush()
160 .map_err(|e| AllSourceError::StorageError(format!("Failed to flush WAL: {e}")))?;
161
162 self.writer
163 .get_ref()
164 .sync_all()
165 .map_err(|e| AllSourceError::StorageError(format!("Failed to sync WAL: {e}")))?;
166 }
167
168 self.size += bytes_written;
169 Ok(bytes_written)
170 }
171
172 fn flush(&mut self) -> Result<()> {
173 self.writer
174 .flush()
175 .map_err(|e| AllSourceError::StorageError(format!("Failed to flush WAL: {}", e)))?;
176 Ok(())
177 }
178}
179
180impl WriteAheadLog {
181 pub fn new(wal_dir: impl Into<PathBuf>, config: WALConfig) -> Result<Self> {
183 let wal_dir = wal_dir.into();
184
185 fs::create_dir_all(&wal_dir).map_err(|e| {
187 AllSourceError::StorageError(format!("Failed to create WAL directory: {e}"))
188 })?;
189
190 let initial_file_path = Self::generate_wal_filename(&wal_dir, 0);
192 let current_file = WALFile::new(initial_file_path)?;
193
194 tracing::info!("โ
WAL initialized at: {}", wal_dir.display());
195
196 Ok(Self {
197 wal_dir,
198 current_file: Arc::new(RwLock::new(current_file)),
199 config,
200 stats: Arc::new(RwLock::new(WALStats::default())),
201 sequence: Arc::new(RwLock::new(0)),
202 replication_tx: parking_lot::Mutex::new(None),
203 })
204 }
205
206 fn generate_wal_filename(dir: &Path, sequence: u64) -> PathBuf {
208 dir.join(format!("wal-{:016x}.log", sequence))
209 }
210
211 pub fn append(&self, event: Event) -> Result<u64> {
213 let mut seq = self.sequence.write();
215 *seq += 1;
216 let sequence = *seq;
217 drop(seq);
218
219 let entry = WALEntry::new(sequence, event);
221
222 let mut current = self.current_file.write();
224 let bytes_written = current.write_entry(&entry, self.config.sync_on_write)?;
225
226 let mut stats = self.stats.write();
228 stats.total_entries += 1;
229 stats.total_bytes_written += bytes_written as u64;
230 stats.current_file_size = current.size;
231 drop(stats);
232
233 if let Some(ref tx) = *self.replication_tx.lock() {
236 let _ = tx.send(entry);
237 }
238
239 let should_rotate = current.size >= self.config.max_file_size;
241 drop(current);
242
243 if should_rotate {
244 self.rotate()?;
245 }
246
247 tracing::trace!("WAL entry written: sequence={}", sequence);
248
249 Ok(sequence)
250 }
251
252 fn rotate(&self) -> Result<()> {
254 let seq = *self.sequence.read();
255 let new_file_path = Self::generate_wal_filename(&self.wal_dir, seq);
256
257 tracing::info!("๐ Rotating WAL to new file: {:?}", new_file_path);
258
259 let new_file = WALFile::new(new_file_path)?;
260
261 let mut current = self.current_file.write();
262 current.flush()?;
263 *current = new_file;
264
265 let mut stats = self.stats.write();
266 stats.files_rotated += 1;
267 stats.current_file_size = 0;
268 drop(stats);
269
270 self.cleanup_old_files()?;
272
273 Ok(())
274 }
275
276 fn cleanup_old_files(&self) -> Result<()> {
278 let mut wal_files = self.list_wal_files()?;
279 wal_files.sort();
280
281 if wal_files.len() > self.config.max_wal_files {
282 let to_remove = wal_files.len() - self.config.max_wal_files;
283 let files_to_delete = &wal_files[..to_remove];
284
285 for file_path in files_to_delete {
286 if let Err(e) = fs::remove_file(file_path) {
287 tracing::warn!("Failed to remove old WAL file {:?}: {}", file_path, e);
288 } else {
289 tracing::debug!("๐๏ธ Removed old WAL file: {:?}", file_path);
290 let mut stats = self.stats.write();
291 stats.files_cleaned += 1;
292 }
293 }
294 }
295
296 Ok(())
297 }
298
299 fn list_wal_files(&self) -> Result<Vec<PathBuf>> {
301 let entries = fs::read_dir(&self.wal_dir).map_err(|e| {
302 AllSourceError::StorageError(format!("Failed to read WAL directory: {e}"))
303 })?;
304
305 let mut wal_files = Vec::new();
306 for entry in entries {
307 let entry = entry.map_err(|e| {
308 AllSourceError::StorageError(format!("Failed to read directory entry: {e}"))
309 })?;
310
311 let path = entry.path();
312 if let Some(name) = path.file_name()
313 && name.to_string_lossy().starts_with("wal-")
314 && name.to_string_lossy().ends_with(".log")
315 {
316 wal_files.push(path);
317 }
318 }
319
320 Ok(wal_files)
321 }
322
323 pub fn recover(&self) -> Result<Vec<Event>> {
325 tracing::info!("๐ Starting WAL recovery...");
326
327 let mut wal_files = self.list_wal_files()?;
328 wal_files.sort();
329
330 let mut recovered_events = Vec::new();
331 let mut max_sequence = 0u64;
332 let mut corrupted_entries = 0;
333
334 for wal_file_path in &wal_files {
335 tracing::debug!("Reading WAL file: {:?}", wal_file_path);
336
337 let file = File::open(wal_file_path).map_err(|e| {
338 AllSourceError::StorageError(format!("Failed to open WAL file for recovery: {e}"))
339 })?;
340
341 let reader = BufReader::new(file);
342
343 for (line_num, line) in reader.lines().enumerate() {
344 let line = line.map_err(|e| {
345 AllSourceError::StorageError(format!("Failed to read WAL line: {e}"))
346 })?;
347
348 if line.trim().is_empty() {
349 continue;
350 }
351
352 match serde_json::from_str::<WALEntry>(&line) {
353 Ok(entry) => {
354 if !entry.verify() {
356 tracing::warn!(
357 "Corrupted WAL entry at {:?}:{} (checksum mismatch)",
358 wal_file_path,
359 line_num + 1
360 );
361 corrupted_entries += 1;
362 continue;
363 }
364
365 max_sequence = max_sequence.max(entry.sequence);
366 recovered_events.push(entry.event);
367 }
368 Err(e) => {
369 tracing::warn!(
370 "Failed to parse WAL entry at {:?}:{}: {}",
371 wal_file_path,
372 line_num + 1,
373 e
374 );
375 corrupted_entries += 1;
376 }
377 }
378 }
379 }
380
381 let mut seq = self.sequence.write();
383 *seq = max_sequence;
384 drop(seq);
385
386 let mut stats = self.stats.write();
388 stats.recovery_count += 1;
389 drop(stats);
390
391 tracing::info!(
392 "โ
WAL recovery complete: {} events recovered, {} corrupted entries",
393 recovered_events.len(),
394 corrupted_entries
395 );
396
397 Ok(recovered_events)
398 }
399
400 pub fn flush(&self) -> Result<()> {
402 let mut current = self.current_file.write();
403 current.flush()?;
404 Ok(())
405 }
406
407 pub fn sync(&self) -> Result<()> {
413 let mut current = self.current_file.write();
414 current
415 .writer
416 .flush()
417 .map_err(|e| AllSourceError::StorageError(format!("Failed to flush WAL: {e}")))?;
418 current
419 .writer
420 .get_ref()
421 .sync_all()
422 .map_err(|e| AllSourceError::StorageError(format!("Failed to sync WAL: {e}")))?;
423 Ok(())
424 }
425
426 pub fn fsync_interval_ms(&self) -> Option<u64> {
428 self.config.fsync_interval_ms
429 }
430
431 pub fn truncate(&self) -> Result<()> {
433 tracing::info!("๐งน Truncating WAL after checkpoint");
434
435 let mut current = self.current_file.write();
437 current.flush()?;
438
439 let wal_files = self.list_wal_files()?;
441 for file_path in wal_files {
442 fs::remove_file(&file_path).map_err(|e| {
443 AllSourceError::StorageError(format!("Failed to remove WAL file: {e}"))
444 })?;
445 tracing::debug!("Removed WAL file: {:?}", file_path);
446 }
447
448 let new_file_path = Self::generate_wal_filename(&self.wal_dir, 0);
450 *current = WALFile::new(new_file_path)?;
451
452 let mut seq = self.sequence.write();
454 *seq = 0;
455
456 tracing::info!("โ
WAL truncated successfully");
457
458 Ok(())
459 }
460
461 pub fn stats(&self) -> WALStats {
463 (*self.stats.read()).clone()
464 }
465
466 pub fn current_sequence(&self) -> u64 {
468 *self.sequence.read()
469 }
470
471 pub fn oldest_sequence(&self) -> Option<u64> {
477 let mut wal_files = match self.list_wal_files() {
478 Ok(files) => files,
479 Err(_) => return None,
480 };
481
482 if wal_files.is_empty() {
483 return None;
484 }
485
486 wal_files.sort();
487
488 for wal_file_path in &wal_files {
490 let file = match File::open(wal_file_path) {
491 Ok(f) => f,
492 Err(_) => continue,
493 };
494 let reader = BufReader::new(file);
495 for line in reader.lines() {
496 let line = match line {
497 Ok(l) => l,
498 Err(_) => continue,
499 };
500 if line.trim().is_empty() {
501 continue;
502 }
503 if let Ok(entry) = serde_json::from_str::<WALEntry>(&line) {
504 return Some(entry.sequence);
505 }
506 }
507 }
508
509 None
510 }
511
512 pub fn set_replication_tx(&self, tx: tokio::sync::broadcast::Sender<WALEntry>) {
517 *self.replication_tx.lock() = Some(tx);
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524 use serde_json::json;
525 use tempfile::TempDir;
526 use uuid::Uuid;
527
528 fn create_test_event() -> Event {
529 Event::reconstruct_from_strings(
530 Uuid::new_v4(),
531 "test.event".to_string(),
532 "test-entity".to_string(),
533 "default".to_string(),
534 json!({"test": "data"}),
535 Utc::now(),
536 None,
537 1,
538 )
539 }
540
541 #[test]
542 fn test_wal_creation() {
543 let temp_dir = TempDir::new().unwrap();
544 let wal = WriteAheadLog::new(temp_dir.path(), WALConfig::default());
545 assert!(wal.is_ok());
546 }
547
548 #[test]
549 fn test_wal_append() {
550 let temp_dir = TempDir::new().unwrap();
551 let wal = WriteAheadLog::new(temp_dir.path(), WALConfig::default()).unwrap();
552
553 let event = create_test_event();
554 let seq = wal.append(event);
555 assert!(seq.is_ok());
556 assert_eq!(seq.unwrap(), 1);
557
558 let stats = wal.stats();
559 assert_eq!(stats.total_entries, 1);
560 }
561
562 #[test]
563 fn test_wal_recovery() {
564 let temp_dir = TempDir::new().unwrap();
565 let wal = WriteAheadLog::new(temp_dir.path(), WALConfig::default()).unwrap();
566
567 for _ in 0..5 {
569 wal.append(create_test_event()).unwrap();
570 }
571
572 wal.flush().unwrap();
573
574 let wal2 = WriteAheadLog::new(temp_dir.path(), WALConfig::default()).unwrap();
576 let recovered = wal2.recover().unwrap();
577
578 assert_eq!(recovered.len(), 5);
579 }
580
581 #[test]
582 fn test_wal_rotation() {
583 let temp_dir = TempDir::new().unwrap();
584 let config = WALConfig {
585 max_file_size: 1024, ..Default::default()
587 };
588
589 let wal = WriteAheadLog::new(temp_dir.path(), config).unwrap();
590
591 for _ in 0..50 {
593 wal.append(create_test_event()).unwrap();
594 }
595
596 let stats = wal.stats();
597 assert!(stats.files_rotated > 0);
598 }
599
600 #[test]
601 fn test_wal_entry_checksum() {
602 let event = create_test_event();
603 let entry = WALEntry::new(1, event);
604
605 assert!(entry.verify());
606
607 let mut corrupted = entry.clone();
609 corrupted.checksum = 0;
610 assert!(!corrupted.verify());
611 }
612
613 #[test]
614 fn test_wal_fsync_interval_config() {
615 let config = WALConfig {
616 fsync_interval_ms: Some(100),
617 ..Default::default()
618 };
619 assert_eq!(config.fsync_interval_ms, Some(100));
620 assert_eq!(WALConfig::default().fsync_interval_ms, None);
622 }
623
624 #[test]
625 fn test_wal_sync_method() {
626 let temp_dir = TempDir::new().unwrap();
627 let config = WALConfig {
628 sync_on_write: false, ..Default::default()
630 };
631 let wal = WriteAheadLog::new(temp_dir.path(), config).unwrap();
632
633 for _ in 0..5 {
635 wal.append(create_test_event()).unwrap();
636 }
637
638 wal.sync().unwrap();
640
641 let wal2 = WriteAheadLog::new(temp_dir.path(), WALConfig::default()).unwrap();
643 let recovered = wal2.recover().unwrap();
644 assert_eq!(recovered.len(), 5);
645 }
646
647 #[test]
648 fn test_wal_truncate() {
649 let temp_dir = TempDir::new().unwrap();
650 let wal = WriteAheadLog::new(temp_dir.path(), WALConfig::default()).unwrap();
651
652 for _ in 0..5 {
654 wal.append(create_test_event()).unwrap();
655 }
656
657 wal.truncate().unwrap();
659
660 assert_eq!(wal.current_sequence(), 0);
662
663 let recovered = wal.recover().unwrap();
665 assert_eq!(recovered.len(), 0);
666 }
667}