Skip to main content

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}