1use std::fs::{File, OpenOptions};
6use std::io::{Read, Seek, SeekFrom, Write};
7use std::path::{Path, PathBuf};
8
9#[cfg(target_os = "linux")]
10use std::os::unix::fs::OpenOptionsExt as _;
11
12use crate::align::{AlignedBuf, DEFAULT_ALIGNMENT, is_aligned};
13use crate::error::{Result, WalError};
14use crate::record::{HEADER_SIZE, WalRecord};
15
16use super::layout::{
17 DWB_CAPACITY, DWB_HEADER_FIELDS, DWB_HEADER_STRIDE, DWB_MAGIC, DWB_SLOT_RECORD_MAX,
18 DWB_SLOT_STRIDE, SlotPrefix, slot_offset,
19};
20use super::metrics;
21use super::mode::DwbMode;
22use super::raw_io::{full_capacity_slice, pwrite_all, zero_tail};
23use super::staging::StagedRing;
24use super::status::{DwbMirror, DwbSkipReason};
25
26pub struct DoubleWriteBuffer {
28 pub(super) file: File,
29 path: PathBuf,
30 pub(super) mode: DwbMode,
31 write_pos: u32,
33 count: u32,
35 next_seq: Option<u64>,
39 pub(super) dirty: bool,
41 stage: Option<StagedRing>,
44 slots_written: u64,
46 header_buf: Option<AlignedBuf>,
48}
49
50impl std::fmt::Debug for DoubleWriteBuffer {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 f.debug_struct("DoubleWriteBuffer")
53 .field("path", &self.path)
54 .field("mode", &self.mode)
55 .field("write_pos", &self.write_pos)
56 .field("count", &self.count)
57 .finish()
58 }
59}
60
61impl DoubleWriteBuffer {
62 pub fn open(path: &Path, mode: DwbMode) -> Result<Self> {
67 if mode == DwbMode::Off {
68 return Err(WalError::DwbOffNotOpenable);
69 }
70
71 let mut opts = OpenOptions::new();
72 opts.read(true).write(true).create(true).truncate(false);
73 #[cfg(target_os = "linux")]
74 if mode == DwbMode::Direct {
75 opts.custom_flags(libc::O_DIRECT);
76 }
77
78 let file = opts.open(path).map_err(|e| {
79 tracing::warn!(path = %path.display(), error = %e, mode = ?mode, "failed to open double-write buffer");
80 WalError::Io(e)
81 })?;
82
83 let header_buf = if mode == DwbMode::Direct {
84 Some(AlignedBuf::new(DWB_HEADER_STRIDE, DEFAULT_ALIGNMENT)?)
85 } else {
86 None
87 };
88
89 let mut dwb = Self {
90 file,
91 path: path.to_path_buf(),
92 mode,
93 write_pos: 0,
94 count: 0,
95 next_seq: None,
96 dirty: false,
97 stage: None,
98 slots_written: 0,
99 header_buf,
100 };
101
102 let file_len = dwb.file.metadata().map(|m| m.len()).unwrap_or(0);
104 if file_len >= DWB_HEADER_STRIDE as u64 {
105 let mut block = vec![0u8; DWB_HEADER_STRIDE];
106 dwb.file.seek(SeekFrom::Start(0)).map_err(WalError::Io)?;
107 if dwb.file.read_exact(&mut block).is_ok() {
108 let mut arr4 = [0u8; 4];
109 arr4.copy_from_slice(&block[0..4]);
110 let magic = u32::from_le_bytes(arr4);
111 if magic == DWB_MAGIC {
112 arr4.copy_from_slice(&block[4..8]);
113 dwb.count = u32::from_le_bytes(arr4);
114 arr4.copy_from_slice(&block[8..12]);
115 dwb.write_pos = u32::from_le_bytes(arr4);
116 }
117 }
118 }
119
120 Ok(dwb)
121 }
122
123 pub fn mode(&self) -> DwbMode {
125 self.mode
126 }
127
128 pub fn path(&self) -> &Path {
130 &self.path
131 }
132
133 pub fn write_record(&mut self, record: &WalRecord) -> Result<DwbMirror> {
139 let mirrored = self.write_record_deferred(record)?;
140 self.flush()?;
141 Ok(mirrored)
142 }
143
144 pub fn write_record_deferred(&mut self, record: &WalRecord) -> Result<DwbMirror> {
155 nodedb_types::fail_point_err!("wal::dwb_write_failure", |detail: String| WalError::Io(
158 std::io::Error::other(format!("failpoint wal::dwb_write_failure: {detail}"))
159 ));
160
161 let total_size = HEADER_SIZE + record.payload.len();
162
163 if total_size > DWB_SLOT_RECORD_MAX {
167 return Ok(DwbMirror::Skipped(DwbSkipReason::RecordTooLarge {
168 size: total_size,
169 max: DWB_SLOT_RECORD_MAX,
170 }));
171 }
172
173 if self.mode == DwbMode::Off {
174 return Err(WalError::DwbOffNotOpenable);
175 }
176
177 let seq = self.take_slot_seq()?;
178 let prefix = SlotPrefix { seq, total_size }.encode();
179 let header_bytes = record.header.to_bytes();
180
181 let stage = match self.stage.take() {
182 Some(stage) => stage,
183 None => StagedRing::new()?,
184 };
185 let stage = self.stage.insert(stage);
186 let image = stage.cleared_image(self.write_pos);
187 image.write(&prefix);
188 image.write(&header_bytes);
189 image.write(&record.payload);
190 stage.mark_staged();
191
192 self.write_pos = self.write_pos.wrapping_add(1);
193 self.count = self.count.saturating_add(1).min(DWB_CAPACITY as u32);
194 self.dirty = true;
195
196 Ok(DwbMirror::Mirrored)
197 }
198
199 pub fn staged_slots(&self) -> u32 {
202 self.stage.as_ref().map_or(0, StagedRing::surviving)
203 }
204
205 pub fn slots_written(&self) -> u64 {
207 self.slots_written
208 }
209
210 pub fn write_staged(&mut self) -> Result<u32> {
216 let Some(stage) = self.stage.as_mut() else {
217 return Ok(0);
218 };
219 let surviving = stage.surviving();
220 let first = self.write_pos.wrapping_sub(surviving);
221 for step in 0..surviving {
222 let idx = first.wrapping_add(step);
223 let offset = slot_offset(idx);
224 let image = stage.image_mut(idx);
225 let bytes = match self.mode {
226 DwbMode::Off => return Err(WalError::DwbOffNotOpenable),
227 DwbMode::Buffered => image.as_slice(),
228 DwbMode::Direct => {
229 zero_tail(image);
232 let slice = full_capacity_slice(image);
233 debug_assert_eq!(slice.len(), DWB_SLOT_STRIDE);
234 debug_assert!(is_aligned(offset as usize, DEFAULT_ALIGNMENT));
235 slice
236 }
237 };
238 pwrite_all(&self.file, bytes, offset)?;
239 metrics::add_bytes_written(bytes.len() as u64);
240 self.slots_written = self.slots_written.saturating_add(1);
241 }
242 stage.clear();
243 Ok(surviving)
244 }
245
246 pub fn flush(&mut self) -> Result<()> {
252 if !self.dirty {
253 return Ok(());
254 }
255
256 self.write_staged()?;
257
258 let mut header = [0u8; DWB_HEADER_FIELDS];
259 header[0..4].copy_from_slice(&DWB_MAGIC.to_le_bytes());
260 header[4..8].copy_from_slice(&self.count.to_le_bytes());
261 header[8..12].copy_from_slice(&self.write_pos.to_le_bytes());
262
263 match self.mode {
264 DwbMode::Off => return Err(WalError::DwbOffNotOpenable),
265 DwbMode::Buffered => {
266 self.file.seek(SeekFrom::Start(0)).map_err(WalError::Io)?;
267 self.file.write_all(&header).map_err(WalError::Io)?;
268 metrics::add_bytes_written(header.len() as u64);
269 }
270 DwbMode::Direct => {
271 let Some(buf) = self.header_buf.as_mut() else {
272 return Err(WalError::AlignmentViolation {
273 context: "DWB Direct mode without an aligned header buffer",
274 required: DWB_HEADER_STRIDE,
275 actual: 0,
276 });
277 };
278 buf.clear();
279 buf.write(&header);
280 zero_tail(buf);
281 let slice = full_capacity_slice(buf);
282 debug_assert_eq!(slice.len(), DWB_HEADER_STRIDE);
283 pwrite_all(&self.file, slice, 0)?;
284 metrics::add_bytes_written(slice.len() as u64);
285 }
286 }
287
288 self.file.sync_all().map_err(WalError::Io)?;
289 self.dirty = false;
290
291 Ok(())
292 }
293
294 fn take_slot_seq(&mut self) -> Result<u64> {
302 let seq = match self.next_seq {
303 Some(seq) => seq,
304 None => super::recover::scan_max_seq(self)?.saturating_add(1),
305 };
306 self.next_seq = Some(seq.saturating_add(1));
307 Ok(seq)
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 use crate::double_write::metrics::wal_dwb_bytes_written_total;
315 use crate::record::{RecordType, WalRecordArgs};
316
317 fn open_buffered(path: &Path) -> DoubleWriteBuffer {
318 DoubleWriteBuffer::open(path, DwbMode::Buffered).unwrap()
319 }
320
321 fn record(lsn: u64, payload: &[u8]) -> WalRecord {
322 WalRecord::new(WalRecordArgs {
323 record_type: RecordType::Put as u32,
324 lsn,
325 tenant_id: 1,
326 vshard_id: 0,
327 database_id: 0,
328 payload: payload.to_vec(),
329 encryption_key: None,
330 preamble_bytes: None,
331 })
332 .unwrap()
333 }
334
335 fn mirror(dwb: &mut DoubleWriteBuffer, rec: &WalRecord) {
336 assert_eq!(dwb.write_record(rec).unwrap(), DwbMirror::Mirrored);
337 }
338
339 fn mirror_deferred(dwb: &mut DoubleWriteBuffer, rec: &WalRecord) {
340 assert_eq!(dwb.write_record_deferred(rec).unwrap(), DwbMirror::Mirrored);
341 }
342
343 #[test]
344 fn write_and_recover() {
345 let dir = tempfile::tempdir().unwrap();
346 let mut dwb = open_buffered(&dir.path().join("test.dwb"));
347
348 mirror(&mut dwb, &record(42, b"hello double-write"));
349
350 let rec = dwb.recover_record(42).unwrap().expect("recoverable");
351 assert_eq!(rec.header.lsn, 42);
352 assert_eq!(rec.payload, b"hello double-write");
353 }
354
355 #[test]
356 fn recover_nonexistent_returns_none() {
357 let dir = tempfile::tempdir().unwrap();
358 let mut dwb = open_buffered(&dir.path().join("test2.dwb"));
359 assert!(dwb.recover_record(999).unwrap().is_none());
360 }
361
362 #[test]
363 fn survives_reopen() {
364 let dir = tempfile::tempdir().unwrap();
365 let path = dir.path().join("reopen.dwb");
366
367 {
368 let mut dwb = open_buffered(&path);
369 mirror(&mut dwb, &record(7, b"durable"));
370 }
371
372 let mut dwb = open_buffered(&path);
373 let recovered = dwb.recover_record(7).unwrap().expect("recoverable");
374 assert_eq!(recovered.payload, b"durable");
375 }
376
377 #[test]
378 fn batch_deferred_writes_and_flush() {
379 let dir = tempfile::tempdir().unwrap();
380 let mut dwb = open_buffered(&dir.path().join("batch.dwb"));
381
382 for lsn in 1..=5u64 {
383 mirror_deferred(&mut dwb, &record(lsn, format!("batch-{lsn}").as_bytes()));
384 }
385
386 assert!(dwb.dirty);
387 dwb.flush().unwrap();
388 assert!(!dwb.dirty);
389
390 for lsn in 1..=5u64 {
391 let recovered = dwb.recover_record(lsn).unwrap().expect("recoverable");
392 assert_eq!(recovered.payload, format!("batch-{lsn}").into_bytes());
393 }
394 }
395
396 #[test]
397 fn flush_is_idempotent() {
398 let dir = tempfile::tempdir().unwrap();
399 let mut dwb = open_buffered(&dir.path().join("idem.dwb"));
400
401 dwb.flush().unwrap();
402 assert!(!dwb.dirty);
403
404 mirror_deferred(&mut dwb, &record(1, b"data"));
405 dwb.flush().unwrap();
406 dwb.flush().unwrap();
407 assert!(!dwb.dirty);
408 }
409
410 #[test]
411 fn oversized_record_is_reported_as_skipped() {
412 let dir = tempfile::tempdir().unwrap();
413 let mut dwb = open_buffered(&dir.path().join("oversized.dwb"));
414
415 let payload = vec![0xabu8; DWB_SLOT_RECORD_MAX];
416 let outcome = dwb.write_record_deferred(&record(11, &payload)).unwrap();
417
418 assert_eq!(
419 outcome,
420 DwbMirror::Skipped(DwbSkipReason::RecordTooLarge {
421 size: HEADER_SIZE + payload.len(),
422 max: DWB_SLOT_RECORD_MAX,
423 })
424 );
425 assert!(dwb.recover_record(11).unwrap().is_none());
426 }
427
428 #[test]
431 fn flush_writes_at_most_one_ring_of_slots() {
432 let dir = tempfile::tempdir().unwrap();
433 let path = dir.path().join("ring.dwb");
434 let mut dwb = open_buffered(&path);
435
436 let total = DWB_CAPACITY as u64 * 3 + 7;
437 for lsn in 1..=total {
438 mirror_deferred(&mut dwb, &record(lsn, format!("rec-{lsn}").as_bytes()));
439 }
440 assert_eq!(dwb.slots_written(), 0, "staging writes nothing to disk");
441 assert_eq!(dwb.staged_slots(), DWB_CAPACITY as u32);
442 dwb.flush().unwrap();
443 assert_eq!(dwb.slots_written(), DWB_CAPACITY as u64);
444 assert_eq!(dwb.staged_slots(), 0);
445
446 let mut reopened = open_buffered(&path);
447 let newest_lost = total - DWB_CAPACITY as u64;
448 for lsn in (newest_lost + 1)..=total {
449 let recovered = reopened.recover_record(lsn).unwrap().expect("recoverable");
450 assert_eq!(recovered.payload, format!("rec-{lsn}").into_bytes());
451 }
452 assert!(reopened.recover_record(newest_lost).unwrap().is_none());
453 }
454
455 #[test]
456 fn flush_writes_one_slot_per_record_below_capacity() {
457 let dir = tempfile::tempdir().unwrap();
458 let mut dwb = open_buffered(&dir.path().join("few.dwb"));
459
460 for lsn in 1..=10u64 {
461 mirror_deferred(&mut dwb, &record(lsn, b"few"));
462 }
463 dwb.flush().unwrap();
464 assert_eq!(dwb.slots_written(), 10);
465
466 mirror_deferred(&mut dwb, &record(11, b"next batch"));
467 dwb.flush().unwrap();
468 assert_eq!(dwb.slots_written(), 11);
469 assert!(dwb.recover_record(11).unwrap().is_some());
470 }
471
472 #[test]
475 fn staged_record_is_recoverable_only_after_flush() {
476 let dir = tempfile::tempdir().unwrap();
477 let path = dir.path().join("staged.dwb");
478 let mut dwb = open_buffered(&path);
479
480 mirror_deferred(&mut dwb, &record(21, b"torn in the wal"));
481 assert!(open_buffered(&path).recover_record(21).unwrap().is_none());
482
483 dwb.flush().unwrap();
484 let recovered = open_buffered(&path)
485 .recover_record(21)
486 .unwrap()
487 .expect("recoverable after flush");
488 assert_eq!(recovered.payload, b"torn in the wal");
489 }
490
491 #[test]
492 fn bytes_written_counter_increments() {
493 let dir = tempfile::tempdir().unwrap();
494 let before = wal_dwb_bytes_written_total();
495
496 let mut dwb = open_buffered(&dir.path().join("counter.dwb"));
497 mirror(&mut dwb, &record(1, b"counted"));
498
499 assert!(wal_dwb_bytes_written_total() > before);
500 }
501}