Skip to main content

nodedb_wal/writer/
config.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use crate::align::DEFAULT_ALIGNMENT;
7use crate::double_write::{DoubleWriteBuffer, DwbDegradation, DwbMode, DwbProtection};
8use crate::time_anchors::TimeAnchors;
9
10/// Default write buffer size: 2 MiB.
11///
12/// This is the batch size for group commit. Records accumulate here until
13/// the buffer is full or `sync()` is called.
14///
15/// Matches `WalTuning::write_buffer_size` default. Override via
16/// `WalWriterConfig::write_buffer_size` at construction time.
17pub const DEFAULT_WRITE_BUFFER_SIZE: usize = 2 * 1024 * 1024;
18
19/// Configuration for the WAL writer.
20#[derive(Debug, Clone)]
21pub struct WalWriterConfig {
22    /// Size of the aligned write buffer (rounded up to alignment).
23    pub write_buffer_size: usize,
24
25    /// O_DIRECT alignment (typically 4096 for NVMe).
26    pub alignment: usize,
27
28    /// Whether to use O_DIRECT. Set to `false` for testing on filesystems
29    /// that don't support it (e.g., tmpfs).
30    pub use_direct_io: bool,
31
32    /// Double-write buffer I/O mode. `None` means "mirror the parent" —
33    /// `Direct` when `use_direct_io` is true, `Buffered` otherwise.
34    /// `Some(DwbMode::Off)` disables the DWB entirely.
35    pub dwb_mode: Option<DwbMode>,
36
37    /// Commit-time anchors. When set, every sync that commits new records
38    /// appends one `TimeAnchor` record to its batch and, once fsynced, records
39    /// it here. `None` writes no anchors.
40    pub time_anchors: Option<Arc<TimeAnchors>>,
41}
42
43impl Default for WalWriterConfig {
44    fn default() -> Self {
45        Self {
46            write_buffer_size: DEFAULT_WRITE_BUFFER_SIZE,
47            alignment: DEFAULT_ALIGNMENT,
48            use_direct_io: true,
49            dwb_mode: None,
50            time_anchors: None,
51        }
52    }
53}
54
55pub(crate) fn resolve_dwb_mode(config: &WalWriterConfig) -> DwbMode {
56    config
57        .dwb_mode
58        .unwrap_or_else(|| DwbMode::default_for_parent(config.use_direct_io))
59}
60
61/// A writer's double-write buffer and the protection standing that goes with
62/// it. Kept together because "no buffer" means two very different things —
63/// deliberately off, or a DWB that failed to open — and only the second one is
64/// a degradation the caller has to know about.
65pub(crate) struct DwbSetup {
66    /// Where the DWB lives, so a degraded writer can reattach later. `None`
67    /// only when the DWB is configured off.
68    pub path: Option<PathBuf>,
69    pub buffer: Option<DoubleWriteBuffer>,
70    pub protection: DwbProtection,
71}
72
73pub(crate) fn open_dwb_for(config: &WalWriterConfig, path: &Path) -> DwbSetup {
74    let mode = resolve_dwb_mode(config);
75    if mode == DwbMode::Off {
76        return DwbSetup {
77            path: None,
78            buffer: None,
79            protection: DwbProtection::Off,
80        };
81    }
82    let dwb_path = path.with_extension("dwb");
83    let (buffer, protection) = match open_dwb_at(&dwb_path, mode) {
84        Some(d) => (Some(d), DwbProtection::Active),
85        None => (None, DwbProtection::Degraded(DwbDegradation::OpenFailed)),
86    };
87    DwbSetup {
88        path: Some(dwb_path),
89        buffer,
90        protection,
91    }
92}
93
94/// Open the DWB file itself. A failure is logged and reported as absence — the
95/// caller records the degradation, and the WAL keeps working either way.
96pub(crate) fn open_dwb_at(dwb_path: &Path, mode: DwbMode) -> Option<DoubleWriteBuffer> {
97    match DoubleWriteBuffer::open(dwb_path, mode) {
98        Ok(dwb) => Some(dwb),
99        Err(e) => {
100            tracing::warn!(
101                path = %dwb_path.display(),
102                error = %e,
103                mode = ?mode,
104                "failed to open DWB — torn-write protection lost for this writer"
105            );
106            None
107        }
108    }
109}
110
111/// Where a reopened segment resumes writing.
112///
113/// Every completed flush ends on an alignment boundary (the batch is padded
114/// with a framed `Noop` record), so an unaligned recovery offset means the
115/// final batch was torn by a crash mid-write. O_DIRECT cannot write at an
116/// unaligned offset, and the torn block was never fsynced — so it was never
117/// acknowledged to any client — so the block is rewritten from its boundary.
118pub fn resume_offset(end_offset: u64, use_direct_io: bool, alignment: usize, path: &Path) -> u64 {
119    if !use_direct_io {
120        return end_offset;
121    }
122    let alignment = alignment as u64;
123    let aligned = end_offset - (end_offset % alignment);
124    if aligned != end_offset {
125        tracing::warn!(
126            path = %path.display(),
127            end_offset,
128            resume_offset = aligned,
129            "WAL segment ends mid-block — discarding the torn final block (never fsynced, never acknowledged)"
130        );
131    }
132    aligned
133}