Skip to main content

typed_store/rocks/
options.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2026 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{
6    collections::BTreeMap,
7    env,
8    sync::atomic::{AtomicBool, Ordering},
9};
10
11use iota_macros::nondeterministic;
12use rocksdb::{BlockBasedOptions, Cache, ReadOptions};
13use sysinfo::{MemoryRefreshKind, RefreshKind, System};
14use tap::TapFallible;
15use tracing::{debug, info, warn};
16
17// Write buffer size per RocksDB instance can be set via the env var below.
18// If the env var is not set, use the default value in MiB.
19const ENV_VAR_DB_WRITE_BUFFER_SIZE: &str = "DB_WRITE_BUFFER_SIZE_MB";
20const DEFAULT_DB_WRITE_BUFFER_SIZE: usize = 1024;
21
22// Write ahead log size per RocksDB instance can be set via the env var below.
23// If the env var is not set, use the default value in MiB.
24const ENV_VAR_DB_WAL_SIZE: &str = "DB_WAL_SIZE_MB";
25const DEFAULT_DB_WAL_SIZE: usize = 1024;
26
27// Environment variable to control behavior of write throughput optimized
28// tables.
29const ENV_VAR_L0_NUM_FILES_COMPACTION_TRIGGER: &str = "L0_NUM_FILES_COMPACTION_TRIGGER";
30const DEFAULT_L0_NUM_FILES_COMPACTION_TRIGGER: usize = 4;
31const DEFAULT_UNIVERSAL_COMPACTION_L0_NUM_FILES_COMPACTION_TRIGGER: usize = 80;
32const ENV_VAR_MAX_WRITE_BUFFER_SIZE_MB: &str = "MAX_WRITE_BUFFER_SIZE_MB";
33const DEFAULT_MAX_WRITE_BUFFER_SIZE_MB: usize = 256;
34const ENV_VAR_MAX_WRITE_BUFFER_NUMBER: &str = "MAX_WRITE_BUFFER_NUMBER";
35const DEFAULT_MAX_WRITE_BUFFER_NUMBER: usize = 6;
36const ENV_VAR_TARGET_FILE_SIZE_BASE_MB: &str = "TARGET_FILE_SIZE_BASE_MB";
37const DEFAULT_TARGET_FILE_SIZE_BASE_MB: usize = 128;
38
39// Set to 1 to disable blob storage for transactions and effects.
40const ENV_VAR_DISABLE_BLOB_STORAGE: &str = "DISABLE_BLOB_STORAGE";
41const ENV_VAR_DB_PARALLELISM: &str = "DB_PARALLELISM";
42
43static FALLOCATE_DISABLED: AtomicBool = AtomicBool::new(false);
44
45/// Stops RocksDB from preallocating file space in databases opened afterwards
46/// with [`default_db_options`] or [`bulk_ingestion_options`].
47///
48/// For tests that run many nodes in one process: on Linux, each write-ahead
49/// log otherwise reserves up to 1.1 × the write buffer size on disk.
50pub fn disable_fallocate() {
51    FALLOCATE_DISABLED.store(true, Ordering::Relaxed);
52}
53
54/// Returns the RocksDB default options, with `allow_fallocate` turned off if
55/// [`disable_fallocate`] was called.
56pub(crate) fn base_db_options() -> rocksdb::Options {
57    let mut options = rocksdb::Options::default();
58    if !FALLOCATE_DISABLED.load(Ordering::Relaxed) {
59        return options;
60    }
61    // The crate has no setter for `allow_fallocate`. The parsed copy drops the
62    // objects attached to the original, so this must run before any are set.
63    options
64        .get_options_from_string("allow_fallocate=false")
65        .expect("allow_fallocate should be a valid RocksDB option")
66}
67
68#[derive(Clone, Debug, Default)]
69pub struct ReadWriteOptions {
70    /// When set, debug log the hash of the key and value bytes when inserting
71    /// to this table.
72    pub log_value_hash: bool,
73}
74
75impl ReadWriteOptions {
76    pub fn readopts(&self) -> ReadOptions {
77        ReadOptions::default()
78    }
79
80    pub fn set_log_value_hash(mut self, log_value_hash: bool) -> Self {
81        self.log_value_hash = log_value_hash;
82        self
83    }
84}
85
86#[derive(Default, Clone)]
87pub struct DBOptions {
88    pub options: rocksdb::Options,
89    pub rw_options: ReadWriteOptions,
90}
91
92/// Write options for a record that must be on disk before the write returns,
93/// rather than in the operating system's cache until some later sync.
94pub fn synced_write_options() -> rocksdb::WriteOptions {
95    let mut opts = rocksdb::WriteOptions::default();
96    opts.set_sync(true);
97    opts
98}
99
100/// The smallest SST file buffer limit, so a small memory budget does not
101/// split the rows into many tiny files.
102const MIN_SST_BUFFER_SIZE: usize = 64 << 20;
103
104/// RocksDB options tuned for one-shot bulk ingestion into a freshly created
105/// store, e.g. initial index building, inserting the genesis snapshot, or
106/// restoring a formal snapshot.
107pub struct BulkIngestionOptions {
108    pub db_options: rocksdb::Options,
109    pub column_family_options: DBOptions,
110    /// The memtable memory one column family may take.
111    cf_memory_budget: usize,
112}
113
114impl BulkIngestionOptions {
115    /// How much memory each of `writers` concurrent
116    /// [`SstFileBuffer`](crate::rocks::SstFileBuffer)s may fill before writing
117    /// a file: one column family's memtable budget, shared between them.
118    pub fn sst_buffer_limit(&self, writers: usize) -> usize {
119        (self.cf_memory_budget / writers.max(1)).max(MIN_SST_BUFFER_SIZE)
120    }
121
122    /// Per-table config applying the bulk column-family options to every
123    /// listed table.
124    pub fn table_config(
125        &self,
126        table_names: impl IntoIterator<Item = String>,
127    ) -> DBMapTableConfigMap {
128        DBMapTableConfigMap::new(
129            table_names
130                .into_iter()
131                .map(|name| (name, self.column_family_options.clone()))
132                .collect(),
133        )
134    }
135}
136
137/// Memtable budget for a bulk-ingestion store, as `(db_write_buffer_size,
138/// cf_memory_budget)`: 80% of system RAM across the whole database and 25%
139/// for a single column family.
140fn write_buffer_budget(total_memory_bytes: u64) -> (usize, usize) {
141    // A failed memory probe reports 0, and RocksDB reads a whole-database cap
142    // of 0 as unlimited; a measured limit is used as is, however small.
143    const FALLBACK_TOTAL_MEMORY_BYTES: u64 = 4 << 30;
144
145    let total_memory_bytes = if total_memory_bytes == 0 {
146        FALLBACK_TOTAL_MEMORY_BYTES as f64
147    } else {
148        total_memory_bytes as f64
149    };
150    (
151        (total_memory_bytes * 0.8) as usize,
152        (total_memory_bytes * 0.25) as usize,
153    )
154}
155
156pub fn bulk_ingestion_options() -> BulkIngestionOptions {
157    let total_memory_bytes = available_memory_bytes();
158    let num_cpus = num_cpus::get();
159    let (db_write_buffer_size, cf_memory_budget) = write_buffer_budget(total_memory_bytes);
160
161    let mut db_options = base_db_options();
162
163    // `unordered_write` gives a large speedup for bulk writes; relaxed write
164    // ordering is acceptable because the store is rebuilt from scratch on
165    // failure.
166    db_options.set_unordered_write(true);
167
168    // Allow CPU-intensive flushing to use all cores.
169    db_options.set_max_background_jobs(num_cpus as i32);
170
171    // Upper bound on memtable memory: 80% of RAM, sized so flushing keeps up
172    // with the writers.
173    db_options.set_db_write_buffer_size(db_write_buffer_size);
174
175    // Per-column-family options: Create options with compactions disabled and large
176    // write buffers. Each CF can use up to 25% of system RAM, but total is
177    // still limited by set_db_write_buffer_size configured above.
178    let mut column_family_options = default_db_options();
179    column_family_options
180        .options
181        .set_disable_auto_compactions(true);
182
183    // Disable the write backpressure that kicks in as L0 files build up: `-1`
184    // turns off the compaction and slowdown triggers, and the stop trigger is
185    // pushed out of reach. These are per-column-family options, so they must be
186    // set here rather than on `db_options` to reach the index CFs. Compaction
187    // itself is disabled above, so no L0 files are ever compacted away.
188    column_family_options
189        .options
190        .set_level_zero_file_num_compaction_trigger(-1);
191    column_family_options
192        .options
193        .set_level_zero_slowdown_writes_trigger(-1);
194    column_family_options
195        .options
196        .set_level_zero_stop_writes_trigger(i32::MAX);
197
198    const MIN_BUFFER_SIZE: usize = 64 * 1024 * 1024; // 64 MiB
199    // More CPUs means more parallel flushing capacity, so target more buffers,
200    // but never shrink a buffer below the minimum.
201    let target_buffer_count = num_cpus.max(2);
202    let buffer_size = (cf_memory_budget / target_buffer_count).max(MIN_BUFFER_SIZE);
203    let buffer_count = (cf_memory_budget / buffer_size).clamp(2, target_buffer_count) as i32;
204    column_family_options
205        .options
206        .set_write_buffer_size(buffer_size);
207    column_family_options
208        .options
209        .set_max_write_buffer_number(buffer_count);
210
211    debug!(
212        total_memory_bytes,
213        num_cpus,
214        db_write_buffer_size,
215        buffer_size,
216        buffer_count,
217        "configured bulk ingestion options"
218    );
219
220    BulkIngestionOptions {
221        db_options,
222        column_family_options,
223        cf_memory_budget,
224    }
225}
226
227/// Available memory in bytes, honoring cgroup limits in containerized
228/// environments and falling back to total system memory.
229fn available_memory_bytes() -> u64 {
230    // Under the simulator sysinfo must run off the test thread, where the
231    // intercepted clock and rng would make its probes a source of
232    // non-determinism. No-op outside msim.
233    nondeterministic!({
234        // `RefreshKind::nothing().with_memory(..)` avoids collecting other,
235        // slower stats.
236        let mut sys = System::new_with_specifics(
237            RefreshKind::nothing().with_memory(MemoryRefreshKind::everything()),
238        );
239        sys.refresh_memory();
240
241        if let Some(cgroup_limits) = sys.cgroup_limits() {
242            // `total_memory` is 0 when there is no cgroup limit.
243            if cgroup_limits.total_memory > 0 {
244                debug!(
245                    limit = cgroup_limits.total_memory,
246                    "using cgroup memory limit"
247                );
248                return cgroup_limits.total_memory;
249            }
250        }
251
252        let total = sys.total_memory();
253        debug!(total, "using total system memory");
254        total
255    })
256}
257
258#[derive(Clone)]
259pub struct DBMapTableConfigMap(BTreeMap<String, DBOptions>);
260impl DBMapTableConfigMap {
261    pub fn new(map: BTreeMap<String, DBOptions>) -> Self {
262        Self(map)
263    }
264
265    pub fn to_map(&self) -> BTreeMap<String, DBOptions> {
266        self.0.clone()
267    }
268}
269
270impl DBOptions {
271    // Optimize lookup perf for tables where no scans are performed.
272    // If non-trivial number of values can be > 512B in size, it is beneficial to
273    // also specify optimize_for_large_values_no_scan().
274    pub fn optimize_for_point_lookup(mut self, block_cache_size_mb: usize) -> DBOptions {
275        // NOTE: this overwrites the block options.
276        self.options
277            .optimize_for_point_lookup(block_cache_size_mb as u64);
278        self
279    }
280
281    // Optimize write and lookup perf for tables which are rarely scanned, and have
282    // large values. https://rocksdb.org/blog/2021/05/26/integrated-blob-db.html
283    pub fn optimize_for_large_values_no_scan(mut self, min_blob_size: u64) -> DBOptions {
284        if env::var(ENV_VAR_DISABLE_BLOB_STORAGE).is_ok() {
285            info!("Large value blob storage optimization is disabled via env var.");
286            return self;
287        }
288
289        // Blob settings.
290        self.options.set_enable_blob_files(true);
291        self.options
292            .set_blob_compression_type(rocksdb::DBCompressionType::Lz4);
293        self.options.set_enable_blob_gc(true);
294        // Since each blob can have non-trivial size overhead, and compression does not
295        // work across blobs, set a min blob size in bytes to so small
296        // transactions and effects are kept in sst files.
297        self.options.set_min_blob_size(min_blob_size);
298
299        // Increase write buffer size to 256MiB.
300        let write_buffer_size = read_size_from_env(ENV_VAR_MAX_WRITE_BUFFER_SIZE_MB)
301            .unwrap_or(DEFAULT_MAX_WRITE_BUFFER_SIZE_MB)
302            * 1024
303            * 1024;
304        self.options.set_write_buffer_size(write_buffer_size);
305        // Since large blobs are not in sst files, reduce the target file size and base
306        // level target size.
307        let target_file_size_base = 64 << 20;
308        self.options
309            .set_target_file_size_base(target_file_size_base);
310        // Level 1 default to 64MiB * 4 ~ 256MiB.
311        let max_level_zero_file_num = read_size_from_env(ENV_VAR_L0_NUM_FILES_COMPACTION_TRIGGER)
312            .unwrap_or(DEFAULT_L0_NUM_FILES_COMPACTION_TRIGGER);
313        self.options
314            .set_max_bytes_for_level_base(target_file_size_base * max_level_zero_file_num as u64);
315
316        self
317    }
318
319    // Optimize tables with a mix of lookup and scan workloads.
320    pub fn optimize_for_read(mut self, block_cache_size_mb: usize) -> DBOptions {
321        self.options
322            .set_block_based_table_factory(&get_block_options(block_cache_size_mb, 16 << 10));
323        self
324    }
325
326    // Optimize DB receiving significant insertions.
327    pub fn optimize_db_for_write_throughput(mut self, db_max_write_buffer_gb: u64) -> DBOptions {
328        self.options
329            .set_db_write_buffer_size(db_max_write_buffer_gb as usize * 1024 * 1024 * 1024);
330        self.options
331            .set_max_total_wal_size(db_max_write_buffer_gb * 1024 * 1024 * 1024);
332        self
333    }
334
335    // Optimize tables receiving significant insertions.
336    pub fn optimize_for_write_throughput(mut self) -> DBOptions {
337        // Increase write buffer size to 256MiB.
338        let write_buffer_size = read_size_from_env(ENV_VAR_MAX_WRITE_BUFFER_SIZE_MB)
339            .unwrap_or(DEFAULT_MAX_WRITE_BUFFER_SIZE_MB)
340            * 1024
341            * 1024;
342        self.options.set_write_buffer_size(write_buffer_size);
343        // Increase write buffers to keep to 6 before slowing down writes.
344        let max_write_buffer_number = read_size_from_env(ENV_VAR_MAX_WRITE_BUFFER_NUMBER)
345            .unwrap_or(DEFAULT_MAX_WRITE_BUFFER_NUMBER);
346        self.options
347            .set_max_write_buffer_number(max_write_buffer_number.try_into().unwrap());
348        // Keep 1 write buffer so recent writes can be read from memory.
349        self.options
350            .set_max_write_buffer_size_to_maintain((write_buffer_size).try_into().unwrap());
351
352        // Increase compaction trigger for level 0 to 6.
353        let max_level_zero_file_num = read_size_from_env(ENV_VAR_L0_NUM_FILES_COMPACTION_TRIGGER)
354            .unwrap_or(DEFAULT_L0_NUM_FILES_COMPACTION_TRIGGER);
355        self.options.set_level_zero_file_num_compaction_trigger(
356            max_level_zero_file_num.try_into().unwrap(),
357        );
358        self.options.set_level_zero_slowdown_writes_trigger(
359            (max_level_zero_file_num * 12).try_into().unwrap(),
360        );
361        self.options
362            .set_level_zero_stop_writes_trigger((max_level_zero_file_num * 16).try_into().unwrap());
363
364        // Increase sst file size to 128MiB.
365        self.options
366            .set_target_file_size_base(target_file_size_base());
367
368        // Increase level 1 target size to 256MiB * 6 ~ 1.5GiB.
369        self.options
370            .set_max_bytes_for_level_base((write_buffer_size * max_level_zero_file_num) as u64);
371
372        self
373    }
374
375    // Optimize tables receiving significant insertions, without any deletions.
376    // TODO: merge this function with optimize_for_write_throughput(), and use a
377    // flag to indicate if deletion is received.
378    pub fn optimize_for_write_throughput_no_deletion(mut self) -> DBOptions {
379        // Increase write buffer size to 256MiB.
380        let write_buffer_size = read_size_from_env(ENV_VAR_MAX_WRITE_BUFFER_SIZE_MB)
381            .unwrap_or(DEFAULT_MAX_WRITE_BUFFER_SIZE_MB)
382            * 1024
383            * 1024;
384        self.options.set_write_buffer_size(write_buffer_size);
385        // Increase write buffers to keep to 6 before slowing down writes.
386        let max_write_buffer_number = read_size_from_env(ENV_VAR_MAX_WRITE_BUFFER_NUMBER)
387            .unwrap_or(DEFAULT_MAX_WRITE_BUFFER_NUMBER);
388        self.options
389            .set_max_write_buffer_number(max_write_buffer_number.try_into().unwrap());
390        // Keep 1 write buffer so recent writes can be read from memory.
391        self.options
392            .set_max_write_buffer_size_to_maintain((write_buffer_size).try_into().unwrap());
393
394        // Switch to universal compactions.
395        self.options
396            .set_compaction_style(rocksdb::DBCompactionStyle::Universal);
397        let mut compaction_options = rocksdb::UniversalCompactOptions::default();
398        compaction_options.set_max_size_amplification_percent(10000);
399        compaction_options.set_stop_style(rocksdb::UniversalCompactionStopStyle::Similar);
400        self.options
401            .set_universal_compaction_options(&compaction_options);
402
403        let max_level_zero_file_num = read_size_from_env(ENV_VAR_L0_NUM_FILES_COMPACTION_TRIGGER)
404            .unwrap_or(DEFAULT_UNIVERSAL_COMPACTION_L0_NUM_FILES_COMPACTION_TRIGGER);
405        self.options.set_level_zero_file_num_compaction_trigger(
406            max_level_zero_file_num.try_into().unwrap(),
407        );
408        self.options.set_level_zero_slowdown_writes_trigger(
409            (max_level_zero_file_num * 12).try_into().unwrap(),
410        );
411        self.options
412            .set_level_zero_stop_writes_trigger((max_level_zero_file_num * 16).try_into().unwrap());
413
414        // Increase sst file size to 128MiB.
415        self.options
416            .set_target_file_size_base(target_file_size_base());
417
418        // This should be a no-op for universal compaction but increasing it to be safe.
419        self.options
420            .set_max_bytes_for_level_base((write_buffer_size * max_level_zero_file_num) as u64);
421
422        self
423    }
424
425    // Overrides the block options with different block cache size and block size.
426    pub fn set_block_options(
427        mut self,
428        block_cache_size_mb: usize,
429        block_size_bytes: usize,
430    ) -> DBOptions {
431        self.options
432            .set_block_based_table_factory(&get_block_options(
433                block_cache_size_mb,
434                block_size_bytes,
435            ));
436        self
437    }
438
439    // Disables write stalling and stopping based on pending compaction bytes.
440    pub fn disable_write_throttling(mut self) -> DBOptions {
441        self.options.set_soft_pending_compaction_bytes_limit(0);
442        self.options.set_hard_pending_compaction_bytes_limit(0);
443        self.options.set_level_zero_slowdown_writes_trigger(512);
444        self.options.set_level_zero_stop_writes_trigger(1024);
445        self
446    }
447}
448
449/// Creates a default RocksDB option, to be used when RocksDB option is
450/// unspecified.
451pub fn default_db_options() -> DBOptions {
452    let mut opt = base_db_options();
453
454    // One common issue when running tests on Mac is that the default ulimit is too
455    // low, leading to I/O errors such as "Too many open files". Raising fdlimit
456    // to bypass it.
457    if let Some(limit) = fdlimit::raise_fd_limit() {
458        // on windows raise_fd_limit return None
459        opt.set_max_open_files((limit / 8) as i32);
460    }
461
462    // The table cache is locked for updates and this determines the number
463    // of shards, ie 2^10. Increase in case of lock contentions.
464    opt.set_table_cache_num_shard_bits(10);
465
466    // LSM compression settings
467    opt.set_compression_type(rocksdb::DBCompressionType::Lz4);
468    opt.set_bottommost_compression_type(rocksdb::DBCompressionType::Zstd);
469    opt.set_bottommost_zstd_max_train_bytes(1024 * 1024, true);
470
471    // IOTA uses multiple RocksDB in a node, so total sizes of write buffers and WAL
472    // can be higher than the limits below.
473    //
474    // RocksDB also exposes the option to configure total write buffer size across
475    // multiple instances via `write_buffer_manager`. But the write buffer flush
476    // policy (flushing the buffer receiving the next write) may not work well.
477    // So sticking to per-db write buffer size limit for now.
478    //
479    // The environment variables are only meant to be emergency overrides. They may
480    // go away in future. It is preferable to update the default value, or
481    // override the option in code.
482    opt.set_db_write_buffer_size(
483        read_size_from_env(ENV_VAR_DB_WRITE_BUFFER_SIZE).unwrap_or(DEFAULT_DB_WRITE_BUFFER_SIZE)
484            * 1024
485            * 1024,
486    );
487    opt.set_max_total_wal_size(
488        read_size_from_env(ENV_VAR_DB_WAL_SIZE).unwrap_or(DEFAULT_DB_WAL_SIZE) as u64 * 1024 * 1024,
489    );
490
491    // Num threads for compactions and memtable flushes.
492    opt.increase_parallelism(read_size_from_env(ENV_VAR_DB_PARALLELISM).unwrap_or(8) as i32);
493
494    opt.set_enable_pipelined_write(true);
495
496    // Increase block size to 16KiB.
497    // https://github.com/EighteenZi/rocksdb_wiki/blob/master/Memory-usage-in-RocksDB.md#indexes-and-filter-blocks
498    opt.set_block_based_table_factory(&get_block_options(128, 16 << 10));
499
500    // Set memtable bloomfilter.
501    opt.set_memtable_prefix_bloom_ratio(0.02);
502
503    DBOptions {
504        options: opt,
505        rw_options: ReadWriteOptions::default(),
506    }
507}
508
509fn get_block_options(block_cache_size_mb: usize, block_size_bytes: usize) -> BlockBasedOptions {
510    // Set options mostly similar to those used in optimize_for_point_lookup(),
511    // except non-default binary and hash index, to hopefully reduce lookup
512    // latencies without causing any regression for scanning, with slightly more
513    // memory usages. https://github.com/facebook/rocksdb/blob/11cb6af6e5009c51794641905ca40ce5beec7fee/options/options.cc#L611-L621
514    let mut block_options = BlockBasedOptions::default();
515    // Overrides block size.
516    block_options.set_block_size(block_size_bytes);
517    // Configure a block cache.
518    block_options.set_block_cache(&Cache::new_lru_cache(block_cache_size_mb << 20));
519    // Set a bloomfilter with 1% false positive rate.
520    block_options.set_bloom_filter(10.0, false);
521    // From https://github.com/EighteenZi/rocksdb_wiki/blob/master/Block-Cache.md#caching-index-and-filter-blocks
522    block_options.set_pin_l0_filter_and_index_blocks_in_cache(true);
523    block_options
524}
525
526pub fn list_tables(path: std::path::PathBuf) -> eyre::Result<Vec<String>> {
527    const DB_DEFAULT_CF_NAME: &str = "default";
528
529    let opts = rocksdb::Options::default();
530    rocksdb::DBWithThreadMode::<rocksdb::MultiThreaded>::list_cf(&opts, path)
531        .map_err(|e| e.into())
532        .map(|q| {
533            q.iter()
534                .filter_map(|s| {
535                    // The `default` table is not used
536                    if s != DB_DEFAULT_CF_NAME {
537                        Some(s.clone())
538                    } else {
539                        None
540                    }
541                })
542                .collect()
543        })
544}
545
546/// The size, in bytes, at which compactions under
547/// [`DBOptions::optimize_for_write_throughput`] and
548/// [`DBOptions::optimize_for_write_throughput_no_deletion`] cut their output
549/// files.
550pub fn target_file_size_base() -> u64 {
551    read_size_from_env(ENV_VAR_TARGET_FILE_SIZE_BASE_MB).unwrap_or(DEFAULT_TARGET_FILE_SIZE_BASE_MB)
552        as u64
553        * 1024
554        * 1024
555}
556
557pub fn read_size_from_env(var_name: &str) -> Option<usize> {
558    env::var(var_name)
559        .ok()?
560        .parse::<usize>()
561        .tap_err(|e| {
562            warn!(
563                "Env var {} does not contain valid usize integer: {}",
564                var_name, e
565            )
566        })
567        .ok()
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573
574    /// The memtable budget is a fixed share of the measured memory, with a
575    /// safe fallback when the probe fails.
576    #[test]
577    fn write_buffer_budget_scales_with_available_memory() {
578        const TOTAL: u64 = 64 << 30;
579
580        let (db_one, cf_one) = write_buffer_budget(TOTAL);
581        assert_eq!(db_one, (TOTAL as f64 * 0.8) as usize);
582        assert_eq!(cf_one, (TOTAL as f64 * 0.25) as usize);
583
584        // A failed memory probe must not turn the whole-database cap into
585        // RocksDB's unlimited.
586        let (db_unknown, cf_unknown) = write_buffer_budget(0);
587        assert_eq!(write_buffer_budget(4 << 30), (db_unknown, cf_unknown));
588        assert!(db_unknown > 0 && cf_unknown > 0);
589
590        // A measured limit below the fallback is used as is: budgeting above
591        // a cgroup limit would get the process killed.
592        const SMALL: u64 = 2 << 30;
593        let (db_small, cf_small) = write_buffer_budget(SMALL);
594        assert_eq!(db_small, (SMALL as f64 * 0.8) as usize);
595        assert_eq!(cf_small, (SMALL as f64 * 0.25) as usize);
596        assert!(db_small < db_unknown);
597    }
598}