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}