Skip to main content

rust_rocksdb/
trace.rs

1//! Query tracing and trace replay.
2//!
3//! RocksDB can record the queries a DB serves into a trace file and later play
4//! that file back against a DB. This module holds the parts of that feature
5//! that are not methods on the DB: the options controlling what gets recorded
6//! ([`TraceOptions`], [`BlockCacheTraceOptions`],
7//! [`BlockCacheTraceWriterOptions`]), a reader for the raw records in a trace
8//! file ([`TraceReader`]), and the replay side ([`Replayer`],
9//! [`ReplayOptions`]).
10//!
11//! Wraps `include/rocksdb/trace_reader_writer.h`,
12//! `include/rocksdb/utilities/replayer.h`, and the `TraceOptions` and
13//! `TraceFilterType` declarations in `include/rocksdb/options.h`.
14
15use crate::env::Env;
16use crate::env_options::EnvOptions;
17use crate::ffi_util::{raw_data_and_free, to_cpath};
18use crate::{AsColumnFamilyRef, Error, ffi};
19use libc::c_uchar;
20use std::ffi::CStr;
21use std::marker::PhantomData;
22use std::ops::{BitOr, BitOrAssign};
23use std::path::Path;
24use std::ptr;
25
26/// Which operation types tracing skips.
27///
28/// Every bit *excludes* an operation type from the trace, so
29/// [`TraceFilter::empty`] traces everything and
30/// `TraceFilter::GET | TraceFilter::MULTI_GET` traces everything except point
31/// lookups. Filtering happens before sampling.
32///
33/// Mirrors `TraceFilterType` in `include/rocksdb/options.h`. Bits that this
34/// version of RocksDB does not define are preserved rather than rejected, so a
35/// value read back from [`TraceOptions::get_filter`] always round-trips.
36#[derive(Copy, Clone, Debug, Eq, PartialEq)]
37pub struct TraceFilter(u64);
38
39impl TraceFilter {
40    /// Exclude nothing, the RocksDB default.
41    pub const NONE: Self = Self(ffi::rocksdb_trace_filter_none as u64);
42    /// Exclude `Get`.
43    pub const GET: Self = Self(ffi::rocksdb_trace_filter_get as u64);
44    /// Exclude writes.
45    pub const WRITE: Self = Self(ffi::rocksdb_trace_filter_write as u64);
46    /// Exclude `Iterator::Seek`.
47    pub const ITERATOR_SEEK: Self = Self(ffi::rocksdb_trace_filter_iterator_seek as u64);
48    /// Exclude `Iterator::SeekForPrev`.
49    pub const ITERATOR_SEEK_FOR_PREV: Self =
50        Self(ffi::rocksdb_trace_filter_iterator_seek_for_prev as u64);
51    /// Exclude `MultiGet`.
52    pub const MULTI_GET: Self = Self(ffi::rocksdb_trace_filter_multi_get as u64);
53
54    /// A filter that excludes nothing, the same as [`TraceFilter::NONE`].
55    pub const fn empty() -> Self {
56        Self::NONE
57    }
58
59    /// The raw bitmask, as RocksDB stores it.
60    pub const fn bits(self) -> u64 {
61        self.0
62    }
63
64    /// Builds a filter from a raw bitmask, keeping bits this crate does not
65    /// know about instead of dropping them.
66    pub const fn from_bits_retain(bits: u64) -> Self {
67        Self(bits)
68    }
69
70    /// Whether every bit set in `other` is also set here. Always true when
71    /// `other` is empty.
72    pub const fn contains(self, other: Self) -> bool {
73        self.0 & other.0 == other.0
74    }
75}
76
77impl BitOr for TraceFilter {
78    type Output = Self;
79
80    fn bitor(self, rhs: Self) -> Self {
81        Self(self.0 | rhs.0)
82    }
83}
84
85impl BitOrAssign for TraceFilter {
86    fn bitor_assign(&mut self, rhs: Self) {
87        self.0 |= rhs.0;
88    }
89}
90
91/// Controls what a query trace, IO trace, or block cache trace records.
92///
93/// Passed to the DB when a trace is started. Changing it afterwards has no
94/// effect on a trace that is already running.
95pub struct TraceOptions {
96    pub(crate) inner: *mut ffi::rocksdb_trace_options_t,
97}
98
99impl Default for TraceOptions {
100    fn default() -> Self {
101        let opts = unsafe { ffi::rocksdb_trace_options_create() };
102        assert!(!opts.is_null(), "Could not create RocksDB Trace Options");
103
104        Self { inner: opts }
105    }
106}
107
108impl Drop for TraceOptions {
109    fn drop(&mut self) {
110        unsafe {
111            ffi::rocksdb_trace_options_destroy(self.inner);
112        }
113    }
114}
115
116// SAFETY: the pointee is a plain options bag with no thread affinity, and the
117// setters take `&mut self` so shared access cannot mutate it.
118unsafe impl Send for TraceOptions {}
119unsafe impl Sync for TraceOptions {}
120
121impl TraceOptions {
122    /// Stops the trace once the file reaches this many bytes, so a long trace
123    /// cannot fill the disk.
124    ///
125    /// Default: 64 GiB
126    pub fn set_max_trace_file_size(&mut self, size: u64) {
127        unsafe {
128            ffi::rocksdb_trace_options_set_max_trace_file_size(self.inner, size);
129        }
130    }
131
132    /// Returns the current `max_trace_file_size` setting.
133    ///
134    /// See [`Self::set_max_trace_file_size`] for what this controls.
135    pub fn get_max_trace_file_size(&self) -> u64 {
136        unsafe { ffi::rocksdb_trace_options_get_max_trace_file_size(self.inner) }
137    }
138
139    /// Captures one request out of every `frequency`. Sampling runs after
140    /// filtering.
141    ///
142    /// Default: 1, meaning capture every request.
143    pub fn set_sampling_frequency(&mut self, frequency: u64) {
144        unsafe {
145            ffi::rocksdb_trace_options_set_sampling_frequency(self.inner, frequency);
146        }
147    }
148
149    /// Returns the current `sampling_frequency` setting.
150    ///
151    /// See [`Self::set_sampling_frequency`] for what this controls.
152    pub fn get_sampling_frequency(&self) -> u64 {
153        unsafe { ffi::rocksdb_trace_options_get_sampling_frequency(self.inner) }
154    }
155
156    /// Sets which operation types to leave out of the trace. Note the
157    /// inversion: a bit set here means that operation is *not* recorded.
158    ///
159    /// Default: [`TraceFilter::NONE`], record everything.
160    pub fn set_filter(&mut self, filter: TraceFilter) {
161        unsafe {
162            ffi::rocksdb_trace_options_set_filter(self.inner, filter.bits());
163        }
164    }
165
166    /// Returns the current `filter` setting.
167    ///
168    /// See [`Self::set_filter`] for what this controls.
169    pub fn get_filter(&self) -> TraceFilter {
170        TraceFilter::from_bits_retain(unsafe { ffi::rocksdb_trace_options_get_filter(self.inner) })
171    }
172
173    /// When true, write records land in the trace in the same order they land
174    /// in the WAL. Costs some write throughput.
175    ///
176    /// Default: false, so traced writes may be ordered differently from the WAL.
177    pub fn set_preserve_write_order(&mut self, v: bool) {
178        unsafe {
179            ffi::rocksdb_trace_options_set_preserve_write_order(self.inner, c_uchar::from(v));
180        }
181    }
182
183    /// Returns the current `preserve_write_order` setting.
184    ///
185    /// See [`Self::set_preserve_write_order`] for what this controls.
186    pub fn get_preserve_write_order(&self) -> bool {
187        unsafe { ffi::rocksdb_trace_options_get_preserve_write_order(self.inner) != 0 }
188    }
189}
190
191/// Controls how much of the block cache access stream a block cache trace
192/// records.
193///
194/// This is the newer block cache tracing entry point and is paired with
195/// [`BlockCacheTraceWriterOptions`]. The older entry point reuses
196/// [`TraceOptions`] instead.
197pub struct BlockCacheTraceOptions {
198    pub(crate) inner: *mut ffi::rocksdb_block_cache_trace_options_t,
199}
200
201impl Default for BlockCacheTraceOptions {
202    fn default() -> Self {
203        let opts = unsafe { ffi::rocksdb_block_cache_trace_options_create() };
204        assert!(
205            !opts.is_null(),
206            "Could not create RocksDB Block Cache Trace Options"
207        );
208
209        Self { inner: opts }
210    }
211}
212
213impl Drop for BlockCacheTraceOptions {
214    fn drop(&mut self) {
215        unsafe {
216            ffi::rocksdb_block_cache_trace_options_destroy(self.inner);
217        }
218    }
219}
220
221// SAFETY: the pointee is a plain options bag with no thread affinity, and the
222// setters take `&mut self` so shared access cannot mutate it.
223unsafe impl Send for BlockCacheTraceOptions {}
224unsafe impl Sync for BlockCacheTraceOptions {}
225
226impl BlockCacheTraceOptions {
227    /// Captures one block cache access out of every `frequency`.
228    ///
229    /// Default: 1, meaning capture every access.
230    pub fn set_sampling_frequency(&mut self, frequency: u64) {
231        unsafe {
232            ffi::rocksdb_block_cache_trace_options_set_sampling_frequency(self.inner, frequency);
233        }
234    }
235
236    /// Returns the current `sampling_frequency` setting.
237    ///
238    /// See [`Self::set_sampling_frequency`] for what this controls.
239    pub fn get_sampling_frequency(&self) -> u64 {
240        unsafe { ffi::rocksdb_block_cache_trace_options_get_sampling_frequency(self.inner) }
241    }
242}
243
244/// Controls the file a block cache trace is written to.
245///
246/// Paired with [`BlockCacheTraceOptions`]: one says what to capture, this one
247/// says how to store it.
248pub struct BlockCacheTraceWriterOptions {
249    pub(crate) inner: *mut ffi::rocksdb_block_cache_trace_writer_options_t,
250}
251
252impl Default for BlockCacheTraceWriterOptions {
253    fn default() -> Self {
254        let opts = unsafe { ffi::rocksdb_block_cache_trace_writer_options_create() };
255        assert!(
256            !opts.is_null(),
257            "Could not create RocksDB Block Cache Trace Writer Options"
258        );
259
260        Self { inner: opts }
261    }
262}
263
264impl Drop for BlockCacheTraceWriterOptions {
265    fn drop(&mut self) {
266        unsafe {
267            ffi::rocksdb_block_cache_trace_writer_options_destroy(self.inner);
268        }
269    }
270}
271
272// SAFETY: the pointee is a plain options bag with no thread affinity, and the
273// setters take `&mut self` so shared access cannot mutate it.
274unsafe impl Send for BlockCacheTraceWriterOptions {}
275unsafe impl Sync for BlockCacheTraceWriterOptions {}
276
277impl BlockCacheTraceWriterOptions {
278    /// Stops the block cache trace once the file reaches this many bytes.
279    ///
280    /// Default: 64 GiB
281    pub fn set_max_trace_file_size(&mut self, size: u64) {
282        unsafe {
283            ffi::rocksdb_block_cache_trace_writer_options_set_max_trace_file_size(self.inner, size);
284        }
285    }
286
287    /// Returns the current `max_trace_file_size` setting.
288    ///
289    /// See [`Self::set_max_trace_file_size`] for what this controls.
290    pub fn get_max_trace_file_size(&self) -> u64 {
291        unsafe { ffi::rocksdb_block_cache_trace_writer_options_get_max_trace_file_size(self.inner) }
292    }
293}
294
295/// Reads the raw, still-encoded records of a trace file one at a time.
296///
297/// This is the low level half of tracing: it hands back the bytes RocksDB
298/// wrote, header and footer records included, and does not decode them into
299/// queries. Use [`Replayer`] to run a trace against a DB instead.
300///
301/// Reading is sequential. [`reset`](Self::reset) rewinds to the start of the
302/// file.
303pub struct TraceReader {
304    inner: *mut ffi::rocksdb_trace_reader_t,
305    /// `read` dereferences a null file handle after the reader is closed, so
306    /// this guards it. See [`Self::read`].
307    closed: bool,
308    /// The reader reads through this `Env`, so it has to outlive the reader.
309    _env: Env,
310}
311
312// SAFETY: the pointee is a `FileTraceReader` holding a file handle, a read
313// offset and a scratch buffer, none of which have thread affinity. Every
314// method that touches them takes `&mut self`, so there is no `Sync`
315// counterpart.
316unsafe impl Send for TraceReader {}
317
318impl TraceReader {
319    /// Opens an existing trace file for reading through `env`.
320    ///
321    /// Fails if the file does not exist. Reads use a default [`EnvOptions`]. Use
322    /// [`open_with_env_options`](Self::open_with_env_options) to control how the
323    /// file is read.
324    pub fn open<P: AsRef<Path>>(env: &Env, trace_path: P) -> Result<Self, Error> {
325        Self::open_inner(env, None, trace_path)
326    }
327
328    /// Opens an existing trace file for reading through `env` and `env_opts`.
329    ///
330    /// `env_opts` only has to live for the call. RocksDB reads the options while
331    /// opening the file and does not carry a rate limiter into the reader, so
332    /// unlike [`SstFileWriter::create_with_env_options`] there is nothing here
333    /// that outlives the borrow.
334    ///
335    /// [`SstFileWriter::create_with_env_options`]: crate::SstFileWriter::create_with_env_options
336    pub fn open_with_env_options<P: AsRef<Path>>(
337        env: &Env,
338        env_opts: &EnvOptions,
339        trace_path: P,
340    ) -> Result<Self, Error> {
341        Self::open_inner(env, Some(env_opts), trace_path)
342    }
343
344    fn open_inner<P: AsRef<Path>>(
345        env: &Env,
346        env_opts: Option<&EnvOptions>,
347        trace_path: P,
348    ) -> Result<Self, Error> {
349        let c_path = to_cpath(trace_path)?;
350        let env_opts = env_opts.map_or(ptr::null(), EnvOptions::as_ptr);
351        let reader = unsafe {
352            ffi_try!(ffi::rocksdb_trace_reader_create(
353                env.0.inner,
354                env_opts,
355                c_path.as_ptr(),
356            ))
357        };
358
359        if reader.is_null() {
360            return Err(Error::new("Could not create trace reader.".to_owned()));
361        }
362
363        Ok(Self {
364            inner: reader,
365            closed: false,
366            _env: env.clone(),
367        })
368    }
369
370    /// Reads the next record, or `Ok(None)` at the end of the file.
371    ///
372    /// End of stream is unambiguous here. `FileTraceReader::Read` reports it as
373    /// `Status::Incomplete`, and `rocksdb_trace_reader_read` in `db/c.cc`
374    /// translates that specific status into a null return with no error string
375    /// set, which is the only way a successful call can produce null: a record
376    /// always carries a fixed size header, so a real record is never zero
377    /// bytes, and a short read is reported as `Corruption` instead. Any other
378    /// failure comes back as `Err`.
379    ///
380    /// Returns an error if the reader has been closed, because the C++ `Read`
381    /// dereferences the file handle that [`close`](Self::close) released
382    /// without checking it first.
383    pub fn read(&mut self) -> Result<Option<Vec<u8>>, Error> {
384        if self.closed {
385            return Err(Error::new("TraceReader is closed.".to_owned()));
386        }
387
388        let mut size: usize = 0;
389        let data = unsafe { ffi_try!(ffi::rocksdb_trace_reader_read(self.inner, &raw mut size)) };
390        // The buffer comes from `CopyString` in `db/c.cc`, which `malloc`s it
391        // and hands ownership over, so it is copied out and freed here.
392        Ok(unsafe { raw_data_and_free(data, size) })
393    }
394
395    /// Rewinds to the start of the trace file so it can be read again.
396    ///
397    /// Fails if the reader has been closed.
398    pub fn reset(&mut self) -> Result<(), Error> {
399        unsafe {
400            ffi_try!(ffi::rocksdb_trace_reader_reset(self.inner));
401        }
402        Ok(())
403    }
404
405    /// Releases the underlying file handle and reports any error doing so.
406    ///
407    /// Dropping the reader closes it too, so this is only needed to see a close
408    /// failure. Calling it more than once is a no-op, and reading afterwards
409    /// returns an error.
410    pub fn close(&mut self) -> Result<(), Error> {
411        if self.closed {
412            return Ok(());
413        }
414        // Marked closed before the call because `FileTraceReader::Close`
415        // releases the file handle whatever it ends up returning.
416        self.closed = true;
417        unsafe {
418            ffi_try!(ffi::rocksdb_trace_reader_close(self.inner));
419        }
420        Ok(())
421    }
422}
423
424impl Drop for TraceReader {
425    fn drop(&mut self) {
426        // `rocksdb_trace_reader_destroy` deletes the `TraceReader`, and
427        // `~FileTraceReader` closes it, so an explicit close first would only
428        // repeat work. Close is idempotent either way.
429        unsafe { ffi::rocksdb_trace_reader_destroy(self.inner) }
430    }
431}
432
433/// Controls the pace and parallelism of a [`Replayer::replay`] run.
434pub struct ReplayOptions {
435    pub(crate) inner: *mut ffi::rocksdb_replay_options_t,
436}
437
438impl Default for ReplayOptions {
439    fn default() -> Self {
440        let opts = unsafe { ffi::rocksdb_replay_options_create() };
441        assert!(!opts.is_null(), "Could not create RocksDB Replay Options");
442
443        Self { inner: opts }
444    }
445}
446
447impl Drop for ReplayOptions {
448    fn drop(&mut self) {
449        unsafe {
450            ffi::rocksdb_replay_options_destroy(self.inner);
451        }
452    }
453}
454
455// SAFETY: the pointee is a plain options bag with no thread affinity, and the
456// setters take `&mut self` so shared access cannot mutate it.
457unsafe impl Send for ReplayOptions {}
458unsafe impl Sync for ReplayOptions {}
459
460impl ReplayOptions {
461    /// Number of threads issuing the replayed operations. 0 and 1 both mean
462    /// single threaded.
463    ///
464    /// Default: 1
465    pub fn set_num_threads(&mut self, num_threads: u32) {
466        unsafe {
467            ffi::rocksdb_replay_options_set_num_threads(self.inner, num_threads);
468        }
469    }
470
471    /// Returns the current `num_threads` setting.
472    ///
473    /// See [`Self::set_num_threads`] for what this controls.
474    pub fn get_num_threads(&self) -> u32 {
475        unsafe { ffi::rocksdb_replay_options_get_num_threads(self.inner) }
476    }
477
478    /// Scales the recorded delay between operations. Above 1.0 replays faster
479    /// than real time, between 0.0 and 1.0 slower, 1.0 matches the original
480    /// rate. [`Replayer::replay`] rejects values at or below 0.0.
481    ///
482    /// Default: 1.0
483    pub fn set_fast_forward(&mut self, fast_forward: f64) {
484        unsafe {
485            ffi::rocksdb_replay_options_set_fast_forward(self.inner, fast_forward);
486        }
487    }
488
489    /// Returns the current `fast_forward` setting.
490    ///
491    /// See [`Self::set_fast_forward`] for what this controls.
492    pub fn get_fast_forward(&self) -> f64 {
493        unsafe { ffi::rocksdb_replay_options_get_fast_forward(self.inner) }
494    }
495}
496
497/// Plays a trace file back against a DB.
498///
499/// The replayer keeps raw pointers to the DB and to the column family handles
500/// it was built from and executes operations through them, so `'a` ties it to
501/// the DB it came from.
502///
503/// [`prepare`](Self::prepare) must succeed before [`replay`](Self::replay),
504/// which otherwise fails with `Result incomplete`. Preparing again rewinds the
505/// trace, which is also how to replay a second time after a run has consumed
506/// it.
507pub struct Replayer<'a> {
508    inner: *mut ffi::rocksdb_replayer_t,
509    _db: PhantomData<&'a ()>,
510}
511
512// SAFETY: the pointee is a `ReplayerImpl`, which holds no thread-affine state:
513// its multi-threaded mode builds and joins a thread pool inside the `Replay`
514// call rather than keeping one. The replay cursor it mutates is reached only
515// through `&mut self`, so there is no `Sync` counterpart.
516unsafe impl Send for Replayer<'_> {}
517
518impl Replayer<'_> {
519    /// Builds RocksDB's default replayer for `trace_path`.
520    ///
521    /// An empty `column_families` means the DB's default column family. A
522    /// trace record naming a column family outside the list fails replay with
523    /// `Corruption: Invalid Column Family ID.`, so pass every column family the
524    /// trace touched. A null `env` falls back to the DB's own `Env`, and a null
525    /// `env_options` to RocksDB's defaults.
526    ///
527    /// # Safety
528    ///
529    /// `db` must be a live `rocksdb_t`, the column family handles must belong
530    /// to it, and `env` must be null or a live `rocksdb_env_t`. `'a` must not
531    /// outlive any of them.
532    pub(crate) unsafe fn create_default<'cf, W, I>(
533        db: *mut ffi::rocksdb_t,
534        column_families: I,
535        env: *mut ffi::rocksdb_env_t,
536        env_options: *const ffi::rocksdb_envoptions_t,
537        trace_path: &CStr,
538    ) -> Result<Self, Error>
539    where
540        W: AsColumnFamilyRef + 'cf,
541        I: IntoIterator<Item = &'cf W>,
542    {
543        let mut cf_handles: Vec<_> = column_families
544            .into_iter()
545            .map(AsColumnFamilyRef::inner)
546            .collect();
547        let replayer = unsafe {
548            ffi_try!(ffi::rocksdb_new_default_replayer(
549                db,
550                cf_handles.as_mut_ptr(),
551                cf_handles.len(),
552                env,
553                env_options,
554                trace_path.as_ptr(),
555            ))
556        };
557
558        if replayer.is_null() {
559            return Err(Error::new("Could not create replayer.".to_owned()));
560        }
561
562        Ok(Self {
563            inner: replayer,
564            _db: PhantomData,
565        })
566    }
567
568    /// Reads the trace header and positions the replayer at the first record.
569    ///
570    /// Required before [`replay`](Self::replay). Calling it again rewinds the
571    /// trace and clears the end-of-trace state.
572    pub fn prepare(&mut self) -> Result<(), Error> {
573        unsafe {
574            ffi_try!(ffi::rocksdb_replayer_prepare(self.inner));
575        }
576        Ok(())
577    }
578
579    /// Replays every remaining record against the DB, honouring the recorded
580    /// delay between them as scaled by `options`.
581    ///
582    /// Blocks until the trace is exhausted, then returns `Ok`. Per-operation
583    /// results are discarded: `db/c.cc` passes no result callback, so only an
584    /// overall status comes back. A record whose type this RocksDB build cannot
585    /// execute is skipped rather than failing the run. Once the run reaches the
586    /// end of the trace, further calls fail with `Result incomplete` until
587    /// [`prepare`](Self::prepare) rewinds it.
588    ///
589    /// Fails with `Result incomplete` if `prepare` has not succeeded, and with
590    /// `Invalid argument` if [`ReplayOptions::set_fast_forward`] was given a
591    /// value at or below 0.0.
592    pub fn replay(&mut self, options: &ReplayOptions) -> Result<(), Error> {
593        unsafe {
594            ffi_try!(ffi::rocksdb_replayer_replay(
595                self.inner,
596                options.inner.cast_const(),
597            ));
598        }
599        Ok(())
600    }
601
602    /// The timestamp recorded in the trace header, in microseconds, which is
603    /// when tracing started.
604    ///
605    /// Returns 0 until [`prepare`](Self::prepare) has succeeded, since that is
606    /// what reads the header.
607    pub fn header_timestamp(&self) -> u64 {
608        unsafe { ffi::rocksdb_replayer_get_header_timestamp(self.inner.cast_const()) }
609    }
610}
611
612impl Drop for Replayer<'_> {
613    fn drop(&mut self) {
614        unsafe { ffi::rocksdb_replayer_destroy(self.inner) }
615    }
616}