nodedb_wal/writer/dwb.rs
1// SPDX-License-Identifier: Apache-2.0
2
3//! The writer's side of the double-write buffer: mirroring records into it,
4//! and keeping the loss of torn-write protection visible when it fails.
5//!
6//! A DWB problem deliberately does NOT fail the WAL write that provoked it —
7//! the WAL append is correct on its own and is still acknowledged, and turning
8//! a degraded mode into an outage would be a worse trade. What a DWB problem
9//! does cost is recoverability: a record whose CRC fails with no DWB copy
10//! behind it is indistinguishable from the end of the committed prefix, so a
11//! torn last record in a segment is dropped in silence. That cost is recorded
12//! on the writer and in the process counters instead of being logged once and
13//! forgotten.
14
15use crate::double_write::{DwbDegradation, DwbMirror, DwbProtection, DwbSkipReason, metrics};
16use crate::record::WalRecord;
17
18use super::config::{open_dwb_at, resolve_dwb_mode};
19use super::core::WalWriter;
20
21impl WalWriter {
22 /// Torn-write protection standing of this writer.
23 pub fn dwb_protection(&self) -> DwbProtection {
24 self.dwb_protection
25 }
26
27 /// Records appended to this segment with no double-write copy behind them.
28 ///
29 /// Non-zero means those records cannot be reconstructed if they are torn
30 /// by a power loss at the tail of the segment.
31 pub fn dwb_unprotected_records(&self) -> u64 {
32 self.dwb_unprotected_records
33 }
34
35 /// Reopen a double-write buffer that was detached after a failure.
36 ///
37 /// Detaching keeps a broken device from being hammered once per append,
38 /// but on its own it makes degradation permanent for the life of the
39 /// writer — an operator who has fixed the fault would have no way back
40 /// short of rolling the segment. Returns the resulting standing.
41 pub fn reattach_double_write(&mut self) -> DwbProtection {
42 let Some(dwb_path) = self.dwb_path.clone() else {
43 return self.dwb_protection;
44 };
45 match open_dwb_at(&dwb_path, resolve_dwb_mode(&self.config)) {
46 Some(dwb) => {
47 self.double_write = Some(dwb);
48 self.dwb_protection = DwbProtection::Active;
49 }
50 None => self.degrade_dwb(DwbDegradation::OpenFailed),
51 }
52 self.dwb_protection
53 }
54
55 /// Mirror a record that is already committed to the WAL write buffer.
56 pub(super) fn mirror_into_dwb(&mut self, lsn: u64, record: &WalRecord) {
57 let mirrored = self
58 .double_write
59 .as_mut()
60 .map(|dwb| dwb.write_record_deferred(record));
61 match mirrored {
62 Some(Ok(DwbMirror::Mirrored)) => {}
63 Some(Ok(DwbMirror::Skipped(reason))) => self.note_unprotected_record(lsn, reason),
64 Some(Err(e)) => {
65 tracing::warn!(lsn = lsn, error = %e, "DWB write failed, detaching DWB");
66 self.double_write = None;
67 self.degrade_dwb(DwbDegradation::WriteFailed);
68 self.note_unprotected();
69 }
70 // No buffer attached: either the DWB is off by configuration
71 // (nothing to report) or it already degraded, in which case every
72 // record since is unprotected and has to keep being counted.
73 None => {
74 if self.dwb_protection.is_degraded() {
75 self.note_unprotected();
76 }
77 }
78 }
79 }
80
81 /// Make the mirrored batch durable ahead of the WAL's own fsync.
82 ///
83 /// The staged slots are written first. A failed slot write detaches the
84 /// DWB as a failed record write does, and every staged record that would
85 /// have reached a slot is counted unprotected.
86 ///
87 /// A failed DWB fsync leaves the batch mirrored only in the page cache, so
88 /// its records are unprotected even though they reached slots. The WAL
89 /// sync continues regardless — the WAL's own fsync is what decides
90 /// durability — and the lost protection is recorded.
91 pub(super) fn flush_dwb(&mut self) {
92 let staged = self
93 .double_write
94 .as_ref()
95 .map_or(0, |dwb| dwb.staged_slots());
96 let written = self.double_write.as_mut().map(|dwb| dwb.write_staged());
97 if let Some(Err(e)) = written {
98 tracing::warn!(error = %e, staged, "DWB slot write failed, detaching DWB");
99 self.double_write = None;
100 self.degrade_dwb(DwbDegradation::WriteFailed);
101 for _ in 0..staged {
102 self.note_unprotected();
103 }
104 return;
105 }
106 let flushed = self.double_write.as_mut().map(|dwb| dwb.flush());
107 if let Some(Err(e)) = flushed {
108 tracing::warn!(error = %e, "DWB flush failed, detaching DWB");
109 self.double_write = None;
110 self.degrade_dwb(DwbDegradation::FlushFailed);
111 }
112 }
113
114 /// Account for a record the WAL accepted but the DWB could not mirror.
115 fn note_unprotected_record(&mut self, lsn: u64, reason: DwbSkipReason) {
116 tracing::warn!(
117 lsn = lsn,
118 reason = %reason,
119 "record appended without torn-write protection"
120 );
121 self.note_unprotected();
122 }
123
124 fn note_unprotected(&mut self) {
125 self.dwb_unprotected_records = self.dwb_unprotected_records.saturating_add(1);
126 metrics::add_unprotected_record();
127 }
128
129 /// Record that protection was requested and has now been lost.
130 fn degrade_dwb(&mut self, degradation: DwbDegradation) {
131 if !self.dwb_protection.is_degraded() {
132 metrics::add_degradation();
133 }
134 self.dwb_protection = DwbProtection::Degraded(degradation);
135 }
136}