Skip to main content

rust_rocksdb/
cache.rs

1use crate::{Error, LruCacheOptions, ffi, ffi_util::convert_rocksdb_error};
2use libc::{c_char, c_int, size_t};
3use std::ptr::{self, NonNull};
4use std::sync::Arc;
5
6pub(crate) struct CacheWrapper {
7    pub(crate) inner: NonNull<ffi::rocksdb_cache_t>,
8}
9
10unsafe impl Send for CacheWrapper {}
11unsafe impl Sync for CacheWrapper {}
12
13impl Drop for CacheWrapper {
14    fn drop(&mut self) {
15        unsafe {
16            ffi::rocksdb_cache_destroy(self.inner.as_ptr());
17        }
18    }
19}
20
21#[derive(Clone)]
22pub struct Cache(pub(crate) Arc<CacheWrapper>);
23
24impl Cache {
25    /// Creates an LRU cache with capacity in bytes.
26    pub fn new_lru_cache(capacity: size_t) -> Cache {
27        let inner = NonNull::new(unsafe { ffi::rocksdb_cache_create_lru(capacity) }).unwrap();
28        Cache(Arc::new(CacheWrapper { inner }))
29    }
30
31    /// Creates an LRU cache with custom options.
32    pub fn new_lru_cache_opts(opts: &LruCacheOptions) -> Cache {
33        let inner =
34            NonNull::new(unsafe { ffi::rocksdb_cache_create_lru_opts(opts.inner) }).unwrap();
35        Cache(Arc::new(CacheWrapper { inner }))
36    }
37
38    /// Creates an LRU cache with capacity in bytes that refuses to exceed it.
39    ///
40    /// A normal LRU cache treats the capacity as a target. When an insert
41    /// arrives and nothing more can be evicted, because the remaining entries
42    /// are pinned, it inserts anyway and lets usage go over capacity. With the
43    /// strict limit the insert fails with `MemoryLimit` instead, so the cache
44    /// is a hard memory bound.
45    ///
46    /// That failure is not confined to the cache API. RocksDB inserts into the
47    /// block cache while reading, so operations that need a block the cache
48    /// cannot hold surface the error to the caller. Opening a table file can
49    /// fail the same way when
50    /// [`BlockBasedOptions::set_cache_index_and_filter_blocks`] charges the
51    /// reader against a full cache. Size the cache with headroom before
52    /// turning this on.
53    ///
54    /// [`BlockBasedOptions::set_cache_index_and_filter_blocks`]: crate::BlockBasedOptions::set_cache_index_and_filter_blocks
55    pub fn new_lru_cache_with_strict_capacity_limit(capacity: size_t) -> Cache {
56        let inner = NonNull::new(unsafe {
57            ffi::rocksdb_cache_create_lru_with_strict_capacity_limit(capacity)
58        })
59        .unwrap();
60        Cache(Arc::new(CacheWrapper { inner }))
61    }
62
63    /// Creates a HyperClockCache with `capacity` in bytes.
64    ///
65    /// HyperClockCache is now generally recommended over LRUCache. See RocksDB's
66    /// [HyperClockCacheOptions in cache.h](https://github.com/facebook/rocksdb/blob/main/include/rocksdb/cache.h)
67    /// for details.
68    ///
69    /// `estimated_entry_charge` is an optional parameter. When not provided
70    /// (== 0, recommended and default), an HCC variant with a
71    /// dynamically-growing table and generally good performance is used. This
72    /// variant depends on anonymous mmaps so might not be available on all
73    /// platforms.
74    ///
75    /// If the average "charge" (uncompressed block size) of block cache entries
76    /// is reasonably predicted and provided here, the most efficient variant of
77    /// HCC is used. Performance is degraded if the prediction is inaccurate.
78    /// Prediction could be difficult or impossible with cache-charging features
79    /// such as WriteBufferManager. The best parameter choice based on a cache
80    /// in use is roughly given by `cache.get_usage() / cache.get_occupancy_count()`,
81    /// though it is better to estimate toward the lower side than the higher
82    /// side when the ratio might vary.
83    pub fn new_hyper_clock_cache(capacity: size_t, estimated_entry_charge: size_t) -> Cache {
84        Cache(Arc::new(CacheWrapper {
85            inner: NonNull::new(unsafe {
86                ffi::rocksdb_cache_create_hyper_clock(capacity, estimated_entry_charge)
87            })
88            .unwrap(),
89        }))
90    }
91
92    /// Creates a HyperClockCache with custom options.
93    ///
94    /// Use this over [`Self::new_hyper_clock_cache`] to set the shard count or
95    /// a memory allocator. See [`HyperClockCacheOptions`].
96    pub fn new_hyper_clock_cache_opts(opts: &HyperClockCacheOptions) -> Cache {
97        let inner = NonNull::new(unsafe { ffi::rocksdb_cache_create_hyper_clock_opts(opts.inner) })
98            .unwrap();
99        Cache(Arc::new(CacheWrapper { inner }))
100    }
101
102    /// Returns the cache memory usage in bytes.
103    pub fn get_usage(&self) -> usize {
104        unsafe { ffi::rocksdb_cache_get_usage(self.0.inner.as_ptr()) }
105    }
106
107    /// Returns the pinned memory usage in bytes.
108    pub fn get_pinned_usage(&self) -> usize {
109        unsafe { ffi::rocksdb_cache_get_pinned_usage(self.0.inner.as_ptr()) }
110    }
111
112    /// Returns the configured cache capacity in bytes.
113    pub fn get_capacity(&self) -> usize {
114        unsafe { ffi::rocksdb_cache_get_capacity(self.0.inner.as_ptr()) }
115    }
116
117    /// Returns the number of entries currently occupying the cache hash tables.
118    pub fn get_occupancy_count(&self) -> usize {
119        unsafe { ffi::rocksdb_cache_get_occupancy_count(self.0.inner.as_ptr()) }
120    }
121
122    /// Returns the total number of cache hash table addresses.
123    pub fn get_table_address_count(&self) -> usize {
124        unsafe { ffi::rocksdb_cache_get_table_address_count(self.0.inner.as_ptr()) }
125    }
126
127    /// Sets cache capacity in bytes.
128    pub fn set_capacity(&mut self, capacity: size_t) {
129        unsafe {
130            ffi::rocksdb_cache_set_capacity(self.0.inner.as_ptr(), capacity);
131        }
132    }
133
134    /// Give up the cached data instead of freeing it when the cache goes away.
135    ///
136    /// This is RocksDB's `Cache::DisownData`. It only changes what happens at
137    /// destruction: the last drop stops running the shard destructors, so the
138    /// cached entries are never reclaimed. It exists to make process exit
139    /// faster on a large cache, where walking and freeing every entry costs
140    /// real time and nothing is going to reuse the memory anyway.
141    ///
142    /// [`Cache`] is a reference-counted handle, so this affects the shared cache
143    /// and every other clone of it, not just this handle.
144    ///
145    /// Under ASAN or valgrind RocksDB ignores the request and frees the entries
146    /// as usual, so the leak is not reported there.
147    ///
148    /// # Safety
149    ///
150    /// Every database and every other user of this cache must be dropped
151    /// before this call — RocksDB's contract is: "Always delete the DB object
152    /// before calling this method!" (`advanced_cache.h`). Nothing may read
153    /// from or write to the cache after this call; RocksDB documents any use
154    /// after disowning as unsupported. Anything that would have reused the
155    /// memory later cannot, because it is leaked for the remaining lifetime of
156    /// the process, which makes this call worth doing only shortly before the
157    /// process exits.
158    pub unsafe fn disown_data(&mut self) {
159        unsafe {
160            ffi::rocksdb_cache_disown_data(self.0.inner.as_ptr());
161        }
162    }
163}
164
165pub(crate) struct MemoryAllocatorWrapper {
166    pub(crate) inner: NonNull<ffi::rocksdb_memory_allocator_t>,
167}
168
169unsafe impl Send for MemoryAllocatorWrapper {}
170unsafe impl Sync for MemoryAllocatorWrapper {}
171
172impl Drop for MemoryAllocatorWrapper {
173    fn drop(&mut self) {
174        unsafe {
175            ffi::rocksdb_memory_allocator_destroy(self.inner.as_ptr());
176        }
177    }
178}
179
180/// An allocator RocksDB uses for cache block memory instead of the system one.
181///
182/// Pass it to `set_memory_allocator` on [`LruCacheOptions`] or
183/// [`HyperClockCacheOptions`] before building the cache. One allocator can
184/// back several caches. Cloning is cheap and hands out another reference to
185/// the same allocator.
186///
187/// The allocator is ignored for compression libraries that allocate internally
188/// (currently only XPRESS).
189#[derive(Clone)]
190pub struct MemoryAllocator(pub(crate) Arc<MemoryAllocatorWrapper>);
191
192impl MemoryAllocator {
193    /// Creates a jemalloc allocator that keeps its memory out of core dumps.
194    ///
195    /// The allocator serves every request from one dedicated jemalloc arena
196    /// and marks that arena `MADV_DONTDUMP`. A block cache built on it
197    /// therefore does not land in a core dump, which for a multi-gigabyte
198    /// cache is the difference between a usable dump and an unusable one.
199    /// Using a single arena also cuts jemalloc metadata, and jemalloc's
200    /// thread-local cache is left on to keep the arena's mutex from becoming
201    /// a bottleneck.
202    ///
203    /// # Errors
204    ///
205    /// Fails with `Not implemented: Not compiled with JEMALLOC` when the
206    /// linked librocksdb has no jemalloc support. That depends on how
207    /// librocksdb itself was built, which this crate's `jemalloc` feature
208    /// controls only for a vendored build. An externally supplied librocksdb
209    /// can have jemalloc with the feature off, or lack it with the feature on,
210    /// so check the result rather than the feature.
211    pub fn new_jemalloc_nodump() -> Result<Self, Error> {
212        // `rocksdb_jemalloc_nodump_allocator_create` allocates the handle
213        // before it tries to build the allocator and returns it either way,
214        // so the error path has to destroy it. `ffi_try!` would return first
215        // and leak it.
216        let mut err: *mut c_char = ptr::null_mut();
217        let allocator = unsafe { ffi::rocksdb_jemalloc_nodump_allocator_create(&raw mut err) };
218        if !err.is_null() {
219            if !allocator.is_null() {
220                unsafe { ffi::rocksdb_memory_allocator_destroy(allocator) };
221            }
222            return Err(convert_rocksdb_error(err));
223        }
224
225        let inner = NonNull::new(allocator).ok_or_else(|| {
226            Error::new("Could not create RocksDB jemalloc nodump allocator".to_owned())
227        })?;
228        Ok(MemoryAllocator(Arc::new(MemoryAllocatorWrapper { inner })))
229    }
230
231    /// Returns the raw pointer the C API passes this allocator by.
232    pub(crate) fn as_ptr(&self) -> *mut ffi::rocksdb_memory_allocator_t {
233        self.0.inner.as_ptr()
234    }
235}
236
237/// Configuration for a HyperClockCache, RocksDB's block cache of choice.
238///
239/// HCC is lock free, so it holds up far better than LRU under parallel load
240/// and contention, and it needs sharding only for a modest gain, so its shards
241/// can be much larger and are much less prone to thrashing. Upstream
242/// recommends it over LRU for the block cache.
243///
244/// The caveats, from `rocksdb/include/rocksdb/cache.h`:
245///
246/// * It only works as [`BlockBasedOptions::set_block_cache`]. It is not a
247///   general cache, so it is not usable as a row cache.
248/// * Cache priorities are enforced less aggressively, so a long range scan can
249///   dilute the cache unless it reads with `fill_cache` off.
250/// * Some configurations need anonymous mmap support.
251/// * The bounded counting-CLOCK eviction gives a hit rate slightly different
252///   from LRU's, in either direction.
253///
254/// Build a cache from these with [`Cache::new_hyper_clock_cache_opts`].
255///
256/// [`BlockBasedOptions::set_block_cache`]: crate::BlockBasedOptions::set_block_cache
257pub struct HyperClockCacheOptions {
258    pub(crate) inner: *mut ffi::rocksdb_hyper_clock_cache_options_t,
259}
260
261// Same reasoning as `LruCacheOptions` in `db_options.rs`: the inner pointer is
262// never aliased, and every mutation goes through `&mut self`.
263unsafe impl Send for HyperClockCacheOptions {}
264unsafe impl Sync for HyperClockCacheOptions {}
265
266impl Drop for HyperClockCacheOptions {
267    fn drop(&mut self) {
268        unsafe {
269            ffi::rocksdb_hyper_clock_cache_options_destroy(self.inner);
270        }
271    }
272}
273
274impl HyperClockCacheOptions {
275    /// Creates options for a HyperClockCache of `capacity` bytes.
276    ///
277    /// There is no `Default`, because a cache with no capacity is not a useful
278    /// starting point. See [`Self::set_estimated_entry_charge`] for what to
279    /// pass as `estimated_entry_charge`, and pass 0 if unsure.
280    pub fn new(capacity: size_t, estimated_entry_charge: size_t) -> Self {
281        let inner = unsafe {
282            ffi::rocksdb_hyper_clock_cache_options_create(capacity, estimated_entry_charge)
283        };
284        assert!(
285            !inner.is_null(),
286            "Could not create RocksDB hyper clock cache options"
287        );
288
289        Self { inner }
290    }
291
292    /// Capacity of the cache, in the same units as the `charge` of each entry,
293    /// which is bytes unless entries are charged some other way.
294    pub fn set_capacity(&mut self, capacity: size_t) {
295        unsafe {
296            ffi::rocksdb_hyper_clock_cache_options_set_capacity(self.inner, capacity);
297        }
298    }
299
300    /// The estimated average `charge` of a cache entry, which selects the HCC
301    /// variant.
302    ///
303    /// 0, the recommended value, picks the variant with a dynamically growing
304    /// table and generally good performance. That variant depends on anonymous
305    /// mmap, so it is not available everywhere.
306    ///
307    /// A non-zero value picks the most efficient variant, but only pays off if
308    /// the estimate is close. A bad estimate degrades performance. Estimating
309    /// is hard or impossible with cache-charging features such as
310    /// [`WriteBufferManager`] in play. For a cache already running, roughly
311    /// `cache.get_usage() / cache.get_occupancy_count()` is the right number,
312    /// and when the ratio moves around it is better to guess low than high.
313    ///
314    /// [`WriteBufferManager`]: crate::WriteBufferManager
315    pub fn set_estimated_entry_charge(&mut self, estimated_entry_charge: size_t) {
316        unsafe {
317            ffi::rocksdb_hyper_clock_cache_options_set_estimated_entry_charge(
318                self.inner,
319                estimated_entry_charge,
320            );
321        }
322    }
323
324    /// Cache is sharded into 2^`num_shard_bits` shards, by hash of key.
325    /// If < 0, a good default is chosen based on the capacity and the
326    /// implementation.
327    ///
328    /// HCC uses sharding only for a modest performance boost, so it does not
329    /// need as many shards as LRU does to scale.
330    pub fn set_num_shard_bits(&mut self, num_shard_bits: c_int) {
331        unsafe {
332            ffi::rocksdb_hyper_clock_cache_options_set_num_shard_bits(self.inner, num_shard_bits);
333        }
334    }
335
336    /// Allocates cache block memory through `allocator` instead of the system
337    /// allocator.
338    ///
339    /// These options do not borrow the allocator. The C setter copies the
340    /// `shared_ptr<MemoryAllocator>` out of the handle
341    /// (`opts->rep.memory_allocator = memory_allocator->rep` in `db/c.cc`), so
342    /// the allocator stays alive through the options and the caches built from
343    /// them even after the [`MemoryAllocator`] here is dropped.
344    pub fn set_memory_allocator(&mut self, allocator: &MemoryAllocator) {
345        unsafe {
346            ffi::rocksdb_hyper_clock_cache_options_set_memory_allocator(
347                self.inner,
348                allocator.as_ptr(),
349            );
350        }
351    }
352}