Skip to main content

wal_db/
config.rs

1//! Log configuration.
2
3/// The default maximum record size: 64 MiB.
4///
5/// Generous enough for any realistic single record, small enough that a
6/// corrupt length prefix cannot request a wild allocation.
7const DEFAULT_MAX_RECORD_SIZE: u32 = 64 * 1024 * 1024;
8
9/// How [`Wal::iter`](crate::Wal::iter) reacts to a damaged record.
10///
11/// This governs *iteration*, not the torn-tail truncation that
12/// [`Wal::open`](crate::Wal::open) always performs to keep the append boundary
13/// clean. It matters when a record in the middle of an already-recovered log is
14/// damaged — bit rot, say — and you are reading the log back.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16#[non_exhaustive]
17pub enum RecoveryPolicy {
18    /// Stop at the first damaged record: yield it as a single
19    /// [`WalError::Corruption`](crate::WalError::Corruption), then end.
20    ///
21    /// The default, and the right choice for an append-only log, where a damaged
22    /// record means everything after it is untrustworthy.
23    StopAtFirstError,
24
25    /// Skip past a damaged record and keep going, for forensic or partial
26    /// recovery.
27    ///
28    /// Each damaged record is still yielded as a
29    /// [`WalError::Corruption`](crate::WalError::Corruption) — the loss is never
30    /// silent — but iteration then resumes at the next record. This is only
31    /// possible while a damaged record's length prefix is intact enough to locate
32    /// the next one; a record whose length itself is unreadable (a short read, or
33    /// a length past the maximum) still stops iteration, because there is no way
34    /// to know where the following record begins.
35    SkipBadRecords,
36}
37
38/// Tunable parameters for a [`Wal`](crate::Wal).
39///
40/// `WalConfig` is a builder. Construct it with [`WalConfig::new`] (or
41/// [`Default`]), set the parameters you care about with the `with_*` methods,
42/// and pass it to [`Wal::open_with`](crate::Wal::open_with) or
43/// [`Wal::with_store_and_config`](crate::Wal::with_store_and_config). The
44/// builder methods take and return `self`, so they chain.
45///
46/// New parameters are added here as later milestones land (segment size, sync
47/// policy, group-commit window). The builder shape means those additions do not
48/// break existing call sites.
49///
50/// # Examples
51///
52/// ```
53/// use wal_db::WalConfig;
54///
55/// // Cap records at 1 MiB instead of the 64 MiB default.
56/// let config = WalConfig::new().with_max_record_size(1024 * 1024);
57/// assert_eq!(config.max_record_size(), 1024 * 1024);
58/// ```
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub struct WalConfig {
61    max_record_size: u32,
62    recovery_policy: RecoveryPolicy,
63}
64
65impl WalConfig {
66    /// Start from the defaults.
67    ///
68    /// The only default that matters today is a 64 MiB maximum record size.
69    ///
70    /// ```
71    /// use wal_db::WalConfig;
72    /// let config = WalConfig::new();
73    /// assert_eq!(config.max_record_size(), 64 * 1024 * 1024);
74    /// ```
75    #[must_use]
76    pub const fn new() -> Self {
77        WalConfig {
78            max_record_size: DEFAULT_MAX_RECORD_SIZE,
79            recovery_policy: RecoveryPolicy::StopAtFirstError,
80        }
81    }
82
83    /// Set the largest record the log will accept, in bytes.
84    ///
85    /// [`Wal::append`](crate::Wal::append) rejects any record larger than this
86    /// with [`WalError::RecordTooLarge`](crate::WalError::RecordTooLarge), and
87    /// recovery rejects any on-disk length prefix that claims to be larger
88    /// before reading the payload. That second use is the security-relevant one:
89    /// it bounds the allocation a corrupt or hostile log can request.
90    ///
91    /// ```
92    /// use wal_db::WalConfig;
93    /// let config = WalConfig::new().with_max_record_size(4096);
94    /// assert_eq!(config.max_record_size(), 4096);
95    /// ```
96    #[must_use]
97    pub const fn with_max_record_size(mut self, bytes: u32) -> Self {
98        self.max_record_size = bytes;
99        self
100    }
101
102    /// The configured maximum record size, in bytes.
103    #[must_use]
104    pub const fn max_record_size(self) -> u32 {
105        self.max_record_size
106    }
107
108    /// Set how iteration reacts to a damaged record.
109    ///
110    /// Defaults to [`RecoveryPolicy::StopAtFirstError`].
111    ///
112    /// ```
113    /// use wal_db::{RecoveryPolicy, WalConfig};
114    /// let config = WalConfig::new().with_recovery_policy(RecoveryPolicy::SkipBadRecords);
115    /// assert_eq!(config.recovery_policy(), RecoveryPolicy::SkipBadRecords);
116    /// ```
117    #[must_use]
118    pub const fn with_recovery_policy(mut self, policy: RecoveryPolicy) -> Self {
119        self.recovery_policy = policy;
120        self
121    }
122
123    /// The configured recovery policy.
124    #[must_use]
125    pub const fn recovery_policy(self) -> RecoveryPolicy {
126        self.recovery_policy
127    }
128}
129
130impl Default for WalConfig {
131    fn default() -> Self {
132        Self::new()
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_default_max_record_size_is_64_mib() {
142        assert_eq!(WalConfig::new().max_record_size(), 64 * 1024 * 1024);
143        assert_eq!(WalConfig::default().max_record_size(), 64 * 1024 * 1024);
144    }
145
146    #[test]
147    fn test_with_max_record_size_overrides_default() {
148        let config = WalConfig::new().with_max_record_size(123);
149        assert_eq!(config.max_record_size(), 123);
150    }
151
152    #[test]
153    fn test_config_is_copy_and_eq() {
154        let a = WalConfig::new().with_max_record_size(10);
155        let b = a;
156        assert_eq!(a, b);
157    }
158}