rust_rocksdb/wal_filter.rs
1//! Inspecting and rewriting WAL records during recovery.
2//!
3//! When a DB opens, RocksDB replays every write-ahead log record that has not
4//! yet made it into an SST. A WAL filter sits in that loop: it sees each record
5//! before it is applied and decides whether the record is replayed as written,
6//! skipped, replaced with a different batch, or treated as the end of the
7//! usable log.
8//!
9//! This is a recovery-time hook and nothing else. It runs on the thread inside
10//! [`DB::open`](crate::DB::open), only for records that recovery actually reads,
11//! and never again once the DB is up. RocksDB documents it as single threaded.
12//!
13//! Getting a filter wrong loses writes that were already acknowledged, so the
14//! usual reasons to reach for one are narrow: dropping writes for a column
15//! family that is being retired, rewriting a value encoding that changed
16//! between releases, or cutting recovery short at a known good point after a
17//! bad shutdown.
18//!
19//! Install one with [`Options::set_wal_filter`](crate::Options::set_wal_filter).
20
21use std::ffi::CStr;
22use std::mem::ManuallyDrop;
23use std::panic::{AssertUnwindSafe, catch_unwind};
24use std::process;
25use std::ptr::NonNull;
26use std::slice;
27
28use libc::{c_char, c_int, c_uchar, c_ulonglong, c_void};
29
30use crate::WriteBatch;
31use crate::ffi;
32
33/// What recovery should do with the WAL record that was just handed to the
34/// filter.
35///
36/// Maps onto `WalFilter::WalProcessingOption` in `wal_filter.h`, with
37/// [`Replace`](Self::Replace) covering the case that upstream expresses as
38/// continuing while setting the `batch_changed` out-parameter.
39#[derive(Debug, Copy, Clone, PartialEq, Eq)]
40pub enum WalRecordAction {
41 /// Replay the record unchanged.
42 Continue,
43 /// Replay the batch the filter wrote into `replacement` instead of the
44 /// original record.
45 ///
46 /// The replacement must not grow the record. RocksDB compares the two
47 /// operation counts and fails recovery with `NotSupported` if the
48 /// replacement holds more than the original.
49 Replace,
50 /// Drop this record and carry on with the next one.
51 Ignore,
52 /// Drop this record and stop replaying.
53 ///
54 /// Everything from here on is discarded, including the rest of this log and
55 /// every later log, and it does not come back on a subsequent recovery.
56 StopReplay,
57 /// Report the record as corrupt.
58 ///
59 /// Recovery raises `Status::Corruption` naming this filter. With
60 /// [`paranoid_checks`](crate::Options::set_paranoid_checks) off, RocksDB
61 /// logs the error, drops it, and replays the record anyway.
62 Corrupted,
63}
64
65impl WalRecordAction {
66 /// The `rocksdb_wal_filter_*` constant this maps to.
67 ///
68 /// `Replace` has no constant of its own. It is `continue_processing` plus
69 /// the `batch_changed` flag, which the caller sets separately.
70 fn as_raw(self) -> c_int {
71 let raw = match self {
72 WalRecordAction::Continue | WalRecordAction::Replace => {
73 ffi::rocksdb_wal_filter_continue_processing
74 }
75 WalRecordAction::Ignore => ffi::rocksdb_wal_filter_ignore_current_record,
76 WalRecordAction::StopReplay => ffi::rocksdb_wal_filter_stop_replay,
77 WalRecordAction::Corrupted => ffi::rocksdb_wal_filter_corrupted_record,
78 };
79 raw as c_int
80 }
81}
82
83/// Which log each column family still needs replayed, handed to the filter once
84/// before any records are.
85///
86/// The borrow lasts for the callback only. The C API layer flattens RocksDB's
87/// two `std::map`s into arrays on its own stack and frees them as soon as the
88/// callback returns, so nothing here can be kept.
89pub struct ColumnFamilyLogNumbers<'a> {
90 ids: &'a [u32],
91 log_numbers: &'a [u64],
92 names: &'a [*const c_char],
93 name_lengths: &'a [usize],
94 name_ids: &'a [u32],
95}
96
97impl<'a> ColumnFamilyLogNumbers<'a> {
98 /// Every column family id paired with the log number it was last flushed
99 /// at.
100 ///
101 /// A record from a log older than a family's number is already in an SST
102 /// for that family, which is how a filter decides whether a record still
103 /// matters.
104 pub fn log_numbers(&self) -> impl Iterator<Item = (u32, u64)> + '_ {
105 self.ids
106 .iter()
107 .copied()
108 .zip(self.log_numbers.iter().copied())
109 }
110
111 /// The log number for one column family id.
112 pub fn log_number(&self, cf_id: u32) -> Option<u64> {
113 let at = self.ids.iter().position(|id| *id == cf_id)?;
114 self.log_numbers.get(at).copied()
115 }
116
117 /// Every column family name paired with its id.
118 ///
119 /// Column family handles are not open yet during recovery, so a name is all
120 /// a filter has to go on. Names are raw bytes because RocksDB does not
121 /// require them to be UTF-8.
122 pub fn names(&self) -> impl Iterator<Item = (&'a [u8], u32)> + '_ {
123 let lengths = self.name_lengths.iter().copied();
124 let ids = self.name_ids.iter().copied();
125 self.names
126 .iter()
127 .zip(lengths)
128 .zip(ids)
129 .map(|((name, len), id)| (unsafe { borrowed_slice(name.cast::<u8>(), len) }, id))
130 }
131
132 /// The id of the column family with this name.
133 pub fn id(&self, name: &[u8]) -> Option<u32> {
134 self.names()
135 .find_map(|(candidate, id)| (candidate == name).then_some(id))
136 }
137}
138
139/// A hook into WAL replay.
140///
141/// Implementations are shared: `Options` can be cloned and used to open several
142/// DBs, all of which point at the same filter, so the methods take `&self` and
143/// the trait requires `Send + Sync`. Reach for a `Mutex` or an atomic if the
144/// filter needs to accumulate state.
145///
146/// # Panics
147///
148/// These methods are called from C++ across an `extern "C"` boundary, where an
149/// unwind is undefined behaviour. A panic that escapes any of them aborts the
150/// process instead. Return [`WalRecordAction::Corrupted`] to report a bad
151/// record.
152pub trait WalFilter: Send + Sync {
153 /// Identifies this filter in the LOG file and in the error text RocksDB
154 /// produces when the filter reports corruption.
155 ///
156 /// The pointer behind the returned `CStr` is handed to C++ as is, so it has
157 /// to stay valid for as long as the filter does. A field of `self` or a
158 /// `c"..."` literal both work.
159 fn name(&self) -> &CStr;
160
161 /// Called for each WAL record recovery reads.
162 ///
163 /// `batch` is the record as written. `replacement` starts empty and is only
164 /// looked at if this returns [`WalRecordAction::Replace`], in which case it
165 /// is replayed in place of `batch` and inherits the original's sequence
166 /// number.
167 ///
168 /// `log_file_name` is the path of the log being read, for logging only. It
169 /// is raw bytes because it is built from the DB path, which this crate
170 /// passes through without validating it as UTF-8.
171 fn log_record_found(
172 &self,
173 log_number: u64,
174 log_file_name: &[u8],
175 batch: &WriteBatch,
176 replacement: &mut WriteBatch,
177 ) -> WalRecordAction;
178
179 /// Called once before replay starts, with the flush position of every
180 /// column family.
181 fn column_family_log_number_map(&self, _cf_log_numbers: &ColumnFamilyLogNumbers<'_>) {}
182}
183
184/// Builds a slice from a pointer and length that C++ may hand over as
185/// `(null, 0)`.
186///
187/// `std::vector::data()` is allowed to return null for an empty vector, and
188/// `slice::from_raw_parts` will not accept that.
189unsafe fn borrowed_slice<'a, T>(ptr: *const T, len: usize) -> &'a [T] {
190 if len == 0 {
191 &[]
192 } else {
193 unsafe { slice::from_raw_parts(ptr, len) }
194 }
195}
196
197/// Wraps a batch RocksDB owns so the Rust side can read or write it without
198/// taking on its lifetime.
199///
200/// Both batches in `LogRecordFound` belong to C++. The one being inspected is a
201/// `const WriteBatch&` from the log reader, and the replacement is a stack local
202/// in `rocksdb_walfilter_t::LogRecordFound` that c.cc moves out of afterwards
203/// (c.cc:364, c.cc:372). Running [`WriteBatch`]'s destructor on either would
204/// free memory this crate never allocated, so the wrapper suppresses it.
205///
206/// The caller must pass a live `rocksdb_writebatch_t` and must not let the
207/// result escape the call it came from.
208unsafe fn borrowed_batch(inner: *mut ffi::rocksdb_writebatch_t) -> ManuallyDrop<WriteBatch> {
209 ManuallyDrop::new(WriteBatch { inner })
210}
211
212unsafe extern "C" fn destructor_callback<F: WalFilter>(state: *mut c_void) {
213 unsafe {
214 drop(Box::from_raw(state.cast::<F>()));
215 }
216}
217
218unsafe extern "C" fn name_callback<F: WalFilter>(state: *mut c_void) -> *const c_char {
219 let filter = unsafe { &*state.cast::<F>() };
220 let name = catch_unwind(AssertUnwindSafe(|| filter.name().as_ptr()));
221 let Ok(name) = name else { process::abort() };
222 name
223}
224
225unsafe extern "C" fn log_record_found_callback<F: WalFilter>(
226 state: *mut c_void,
227 log_number: c_ulonglong,
228 log_file_name: *const c_char,
229 log_file_name_len: usize,
230 batch: *const ffi::rocksdb_writebatch_t,
231 new_batch: *mut ffi::rocksdb_writebatch_t,
232 batch_changed: *mut c_uchar,
233) -> c_int {
234 let filter = unsafe { &*state.cast::<F>() };
235 let file_name = unsafe { borrowed_slice(log_file_name.cast::<u8>(), log_file_name_len) };
236
237 // The record arrives as `const rocksdb_writebatch_t*`, but every reader in
238 // the C API takes a non-const pointer, so the cast is unavoidable and is
239 // the same one c.cc performs to produce this argument (c.cc:363). Handing
240 // out `&WriteBatch` keeps the filter to the read-only half of the API.
241 let existing = unsafe { borrowed_batch(batch.cast_mut()) };
242 let mut replacement = unsafe { borrowed_batch(new_batch) };
243
244 let action = catch_unwind(AssertUnwindSafe(|| {
245 filter.log_record_found(log_number, file_name, &existing, &mut replacement)
246 }));
247 let Ok(action) = action else { process::abort() };
248
249 unsafe {
250 *batch_changed = u8::from(action == WalRecordAction::Replace);
251 }
252 action.as_raw()
253}
254
255unsafe extern "C" fn column_family_log_number_map_callback<F: WalFilter>(
256 state: *mut c_void,
257 column_family_ids: *const u32,
258 log_numbers: *const u64,
259 column_family_log_number_count: usize,
260 column_family_names: *const *const c_char,
261 column_family_name_lengths: *const usize,
262 column_family_name_ids: *const u32,
263 column_family_name_count: usize,
264) {
265 let filter = unsafe { &*state.cast::<F>() };
266 let cf_log_numbers = unsafe {
267 ColumnFamilyLogNumbers {
268 ids: borrowed_slice(column_family_ids, column_family_log_number_count),
269 log_numbers: borrowed_slice(log_numbers, column_family_log_number_count),
270 names: borrowed_slice(column_family_names, column_family_name_count),
271 name_lengths: borrowed_slice(column_family_name_lengths, column_family_name_count),
272 name_ids: borrowed_slice(column_family_name_ids, column_family_name_count),
273 }
274 };
275
276 if catch_unwind(AssertUnwindSafe(|| {
277 filter.column_family_log_number_map(&cf_log_numbers);
278 }))
279 .is_err()
280 {
281 process::abort();
282 }
283}
284
285/// Holds a `rocksdb_walfilter_t` and destroys it when dropped.
286///
287/// `rocksdb_options_set_wal_filter` stores the bare pointer in
288/// `DBOptions::wal_filter` (c.cc:5843) and RocksDB never takes ownership of it,
289/// so this has to outlive both the options and every DB opened from them.
290/// [`Options`](crate::Options) keeps it alive through `OptionsMustOutliveDB`.
291pub(crate) struct OwnedWalFilter {
292 inner: NonNull<ffi::rocksdb_walfilter_t>,
293}
294
295impl OwnedWalFilter {
296 pub(crate) fn as_ptr(&self) -> *mut ffi::rocksdb_walfilter_t {
297 self.inner.as_ptr()
298 }
299}
300
301impl Drop for OwnedWalFilter {
302 fn drop(&mut self) {
303 unsafe {
304 ffi::rocksdb_walfilter_destroy(self.inner.as_ptr());
305 }
306 }
307}
308
309// The only things behind the pointer are the callback table and the boxed `F`
310// (c.cc:299), and `WalFilter` requires `Send + Sync`, so the state can be
311// reached from any thread. Nothing mutates the handle after
312// `rocksdb_walfilter_create`, and destruction happens once, when the last `Arc`
313// holding it drops.
314unsafe impl Send for OwnedWalFilter {}
315unsafe impl Sync for OwnedWalFilter {}
316
317pub(crate) fn new_wal_filter<F: WalFilter + 'static>(filter: F) -> OwnedWalFilter {
318 let state = Box::into_raw(Box::new(filter)).cast::<c_void>();
319 let inner = unsafe {
320 ffi::rocksdb_walfilter_create(
321 state,
322 Some(destructor_callback::<F>),
323 Some(column_family_log_number_map_callback::<F>),
324 Some(log_record_found_callback::<F>),
325 Some(name_callback::<F>),
326 )
327 };
328 OwnedWalFilter {
329 inner: NonNull::new(inner).expect("rocksdb_walfilter_create returned null"),
330 }
331}