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}