Skip to main content

linera_views/backends/
rocks_db.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Implements [`crate::store::KeyValueStore`] for the RocksDB database.
5
6use std::{
7    ffi::OsString,
8    fmt::Display,
9    path::PathBuf,
10    sync::{
11        atomic::{AtomicBool, Ordering},
12        Arc,
13    },
14};
15
16use linera_base::ensure;
17use rocksdb::{BlockBasedOptions, Cache, DBCompactionStyle, SliceTransform, WriteBufferManager};
18use serde::{Deserialize, Serialize};
19use sysinfo::{MemoryRefreshKind, RefreshKind, System};
20use tempfile::TempDir;
21use thiserror::Error;
22
23#[cfg(with_metrics)]
24use crate::metering::MeteredDatabase;
25#[cfg(with_testing)]
26use crate::store::TestKeyValueDatabase;
27use crate::{
28    batch::{Batch, WriteOperation},
29    common::get_upper_bound_option,
30    lru_caching::{LruCachingConfig, LruCachingDatabase},
31    store::{
32        KeyValueDatabase, KeyValueStoreError, ReadableKeyValueStore, WithError,
33        WritableKeyValueStore,
34    },
35    value_splitting::{ValueSplittingDatabase, ValueSplittingError},
36};
37
38/// The prefixes being used in the system
39static ROOT_KEY_DOMAIN: [u8; 1] = [0];
40static STORED_ROOT_KEYS_PREFIX: u8 = 1;
41
42/// The number of streams for the test
43#[cfg(with_testing)]
44const TEST_ROCKS_DB_MAX_STREAM_QUERIES: usize = 10;
45
46// The maximum size of values in RocksDB is 3 GiB
47// For offset reasons we decrease by 400
48const MAX_VALUE_SIZE: usize = 3 * 1024 * 1024 * 1024 - 400;
49
50// The maximum size of keys in RocksDB is 8 MiB
51// For offset reasons we decrease by 400
52const MAX_KEY_SIZE: usize = 8 * 1024 * 1024 - 400;
53
54// A small write buffer keeps the memtable flushing even on low-write workloads. A large buffer
55// (e.g. 256 MiB) lets a slowly-filling memtable grow huge and accumulate range tombstones, so every
56// point read has to scan it — which severely amplifies reads during e.g. a long chain
57// synchronization, where blocks are re-executed with little net data written.
58const WRITE_BUFFER_SIZE: usize = 16 * 1024 * 1024; // 16 MiB
59const MAX_WRITE_BUFFER_NUMBER: i32 = 6;
60
61fn get_available_memory(sys: &System) -> usize {
62    sys.cgroup_limits()
63        .map_or_else(|| sys.total_memory() as usize, |c| c.total_memory as usize)
64}
65
66fn get_available_cpus() -> i32 {
67    std::thread::available_parallelism().map_or(1, |p| p.get() as i32)
68}
69
70const HYPER_CLOCK_CACHE_BLOCK_SIZE: usize = 8 * 1024; // 8 KiB
71
72/// The RocksDB client that we use.
73type DB = rocksdb::DBWithThreadMode<rocksdb::MultiThreaded>;
74
75/// The choice of the spawning mode.
76/// `SpawnBlocking` always works and is the safest.
77/// `BlockInPlace` can only be used in multi-threaded environment.
78/// One way to select that is to select BlockInPlace when
79/// `tokio::runtime::Handle::current().metrics().num_workers() > 1`
80/// `BlockInPlace` is documented in <https://docs.rs/tokio/latest/tokio/task/fn.block_in_place.html>
81#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
82pub enum RocksDbSpawnMode {
83    /// This uses the `spawn_blocking` function of Tokio.
84    SpawnBlocking,
85    /// This uses the `block_in_place` function of Tokio.
86    BlockInPlace,
87}
88
89impl RocksDbSpawnMode {
90    /// Obtains the spawning mode from runtime.
91    pub fn get_spawn_mode_from_runtime() -> Self {
92        if tokio::runtime::Handle::current().metrics().num_workers() > 1 {
93            RocksDbSpawnMode::BlockInPlace
94        } else {
95            RocksDbSpawnMode::SpawnBlocking
96        }
97    }
98
99    /// Runs the computation for a function according to the selected policy.
100    #[inline]
101    async fn spawn<F, I, O>(&self, f: F, input: I) -> Result<O, RocksDbStoreInternalError>
102    where
103        F: FnOnce(I) -> Result<O, RocksDbStoreInternalError> + Send + 'static,
104        I: Send + 'static,
105        O: Send + 'static,
106    {
107        Ok(match self {
108            RocksDbSpawnMode::BlockInPlace => tokio::task::block_in_place(move || f(input))?,
109            RocksDbSpawnMode::SpawnBlocking => {
110                tokio::task::spawn_blocking(move || f(input)).await??
111            }
112        })
113    }
114}
115
116impl Display for RocksDbSpawnMode {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        match &self {
119            RocksDbSpawnMode::SpawnBlocking => write!(f, "spawn_blocking"),
120            RocksDbSpawnMode::BlockInPlace => write!(f, "block_in_place"),
121        }
122    }
123}
124
125fn check_key_size(key: &[u8]) -> Result<(), RocksDbStoreInternalError> {
126    ensure!(
127        key.len() <= MAX_KEY_SIZE,
128        RocksDbStoreInternalError::KeyTooLong
129    );
130    Ok(())
131}
132
133#[derive(Clone)]
134struct RocksDbStoreExecutor {
135    db: Arc<DB>,
136    start_key: Vec<u8>,
137}
138
139impl RocksDbStoreExecutor {
140    fn contains_keys_internal(
141        &self,
142        keys: Vec<Vec<u8>>,
143    ) -> Result<Vec<bool>, RocksDbStoreInternalError> {
144        let size = keys.len();
145        let mut results = vec![false; size];
146        let mut indices = Vec::new();
147        let mut keys_red = Vec::new();
148        for (i, key) in keys.into_iter().enumerate() {
149            check_key_size(&key)?;
150            let mut full_key = self.start_key.to_vec();
151            full_key.extend(key);
152            if self.db.key_may_exist(&full_key) {
153                indices.push(i);
154                keys_red.push(full_key);
155            }
156        }
157        let values_red = self.db.multi_get(keys_red);
158        for (index, value) in indices.into_iter().zip(values_red) {
159            results[index] = value?.is_some();
160        }
161        Ok(results)
162    }
163
164    fn read_multi_values_bytes_internal(
165        &self,
166        keys: Vec<Vec<u8>>,
167    ) -> Result<Vec<Option<Vec<u8>>>, RocksDbStoreInternalError> {
168        for key in &keys {
169            check_key_size(key)?;
170        }
171        let full_keys = keys
172            .into_iter()
173            .map(|key| {
174                let mut full_key = self.start_key.to_vec();
175                full_key.extend(key);
176                full_key
177            })
178            .collect::<Vec<_>>();
179        let entries = self.db.multi_get(&full_keys);
180        Ok(entries.into_iter().collect::<Result<_, _>>()?)
181    }
182
183    fn get_find_prefix_iterator(
184        &self,
185        prefix: &[u8],
186    ) -> rocksdb::DBRawIteratorWithThreadMode<'_, DB> {
187        // Configure ReadOptions optimized for SSDs and iterator performance
188        let mut read_opts = rocksdb::ReadOptions::default();
189        // Enable async I/O for better concurrency
190        read_opts.set_async_io(true);
191
192        // Set precise upper bound to minimize key traversal
193        let upper_bound = get_upper_bound_option(prefix);
194        if let Some(upper_bound) = upper_bound {
195            read_opts.set_iterate_upper_bound(upper_bound);
196        }
197
198        let mut iter = self.db.raw_iterator_opt(read_opts);
199        iter.seek(prefix);
200        iter
201    }
202
203    fn find_keys_by_prefix_internal(
204        &self,
205        key_prefix: Vec<u8>,
206    ) -> Result<Vec<Vec<u8>>, RocksDbStoreInternalError> {
207        check_key_size(&key_prefix)?;
208
209        let mut prefix = self.start_key.clone();
210        prefix.extend(key_prefix);
211        let len = prefix.len();
212
213        let mut iter = self.get_find_prefix_iterator(&prefix);
214        let mut keys = Vec::new();
215        while let Some(key) = iter.key() {
216            keys.push(key[len..].to_vec());
217            iter.next();
218        }
219        Ok(keys)
220    }
221
222    #[expect(clippy::type_complexity)]
223    fn find_key_values_by_prefix_internal(
224        &self,
225        key_prefix: Vec<u8>,
226    ) -> Result<Vec<(Vec<u8>, Vec<u8>)>, RocksDbStoreInternalError> {
227        check_key_size(&key_prefix)?;
228        let mut prefix = self.start_key.clone();
229        prefix.extend(key_prefix);
230        let len = prefix.len();
231
232        let mut iter = self.get_find_prefix_iterator(&prefix);
233        let mut key_values = Vec::new();
234        while let Some((key, value)) = iter.item() {
235            let key_value = (key[len..].to_vec(), value.to_vec());
236            key_values.push(key_value);
237            iter.next();
238        }
239        Ok(key_values)
240    }
241
242    fn write_batch_internal(
243        &self,
244        batch: Batch,
245        write_root_key: bool,
246    ) -> Result<(), RocksDbStoreInternalError> {
247        let mut inner_batch = rocksdb::WriteBatchWithTransaction::default();
248        for operation in batch.operations {
249            match operation {
250                WriteOperation::Delete { key } => {
251                    check_key_size(&key)?;
252                    let mut full_key = self.start_key.to_vec();
253                    full_key.extend(key);
254                    inner_batch.delete(&full_key)
255                }
256                WriteOperation::Put { key, value } => {
257                    check_key_size(&key)?;
258                    let mut full_key = self.start_key.to_vec();
259                    full_key.extend(key);
260                    inner_batch.put(&full_key, value)
261                }
262                WriteOperation::DeletePrefix { key_prefix } => {
263                    check_key_size(&key_prefix)?;
264                    let mut full_key1 = self.start_key.to_vec();
265                    full_key1.extend(&key_prefix);
266                    let full_key2 =
267                        get_upper_bound_option(&full_key1).expect("the first entry cannot be 255");
268                    inner_batch.delete_range(&full_key1, &full_key2);
269                }
270            }
271        }
272        if write_root_key {
273            let mut full_key = self.start_key.to_vec();
274            full_key[0] = STORED_ROOT_KEYS_PREFIX;
275            inner_batch.put(&full_key, vec![]);
276        }
277        self.db.write(inner_batch)?;
278        Ok(())
279    }
280}
281
282/// The inner client
283#[derive(Clone)]
284pub struct RocksDbStoreInternal {
285    executor: RocksDbStoreExecutor,
286    path_with_guard: PathWithGuard,
287    max_stream_queries: usize,
288    spawn_mode: RocksDbSpawnMode,
289    root_key_written: Arc<AtomicBool>,
290}
291
292/// Database-level connection to RocksDB for managing namespaces and partitions.
293#[derive(Clone)]
294pub struct RocksDbDatabaseInternal {
295    executor: RocksDbStoreExecutor,
296    path_with_guard: PathWithGuard,
297    max_stream_queries: usize,
298    spawn_mode: RocksDbSpawnMode,
299}
300
301impl WithError for RocksDbDatabaseInternal {
302    type Error = RocksDbStoreInternalError;
303}
304
305/// The level of detail collected by RocksDB's internal statistics.
306///
307/// This mirrors [`rocksdb::statistics::StatsLevel`]. The levels are nested: each one
308/// collects a superset of the data collected by the previous one, at increasing cost.
309#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize, strum::EnumString)]
310#[serde(rename_all = "kebab-case")]
311#[strum(serialize_all = "kebab-case")]
312pub enum RocksDbStatisticsLevel {
313    /// Collect nothing.
314    DisableAll,
315    /// Collect tickers (counters) only; skip all histograms and timers.
316    #[default]
317    ExceptHistogramOrTimers,
318    /// Collect tickers and histograms, but skip timer statistics.
319    ExceptTimers,
320    /// Collect everything except time spent inside the mutex lock and on compression.
321    ExceptDetailedTimers,
322    /// Collect everything except the counters that require taking time inside the mutex lock.
323    ExceptTimeForMutex,
324    /// Collect everything, including the duration of mutex operations.
325    All,
326}
327
328impl RocksDbStatisticsLevel {
329    fn to_rocksdb(self) -> rocksdb::statistics::StatsLevel {
330        use rocksdb::statistics::StatsLevel;
331        match self {
332            Self::DisableAll => StatsLevel::DisableAll,
333            Self::ExceptHistogramOrTimers => StatsLevel::ExceptHistogramOrTimers,
334            Self::ExceptTimers => StatsLevel::ExceptTimers,
335            Self::ExceptDetailedTimers => StatsLevel::ExceptDetailedTimers,
336            Self::ExceptTimeForMutex => StatsLevel::ExceptTimeForMutex,
337            Self::All => StatsLevel::All,
338        }
339    }
340}
341
342#[cfg(test)]
343mod statistics_level_tests {
344    use std::str::FromStr as _;
345
346    use super::RocksDbStatisticsLevel;
347
348    #[test]
349    fn parses_kebab_case_names() {
350        let cases = [
351            ("disable-all", RocksDbStatisticsLevel::DisableAll),
352            (
353                "except-histogram-or-timers",
354                RocksDbStatisticsLevel::ExceptHistogramOrTimers,
355            ),
356            ("except-timers", RocksDbStatisticsLevel::ExceptTimers),
357            (
358                "except-detailed-timers",
359                RocksDbStatisticsLevel::ExceptDetailedTimers,
360            ),
361            (
362                "except-time-for-mutex",
363                RocksDbStatisticsLevel::ExceptTimeForMutex,
364            ),
365            ("all", RocksDbStatisticsLevel::All),
366        ];
367        for (name, expected) in cases {
368            assert_eq!(RocksDbStatisticsLevel::from_str(name), Ok(expected));
369        }
370        assert!(RocksDbStatisticsLevel::from_str("not-a-level").is_err());
371    }
372}
373
374/// The initial configuration of the system
375#[derive(Clone, Debug, Deserialize, Serialize)]
376pub struct RocksDbStoreInternalConfig {
377    /// The path to the storage containing the namespaces
378    pub path_with_guard: PathWithGuard,
379    /// The chosen spawn mode
380    pub spawn_mode: RocksDbSpawnMode,
381    /// Preferred buffer size for async streams.
382    pub max_stream_queries: usize,
383    /// Whether to enable RocksDB's internal statistics collection and export it as
384    /// Prometheus metrics. Disabled by default to avoid overhead in clients that do not
385    /// scrape metrics; enabled explicitly for the workers.
386    #[serde(default)]
387    pub enable_statistics: bool,
388    /// The level of detail collected when `enable_statistics` is set.
389    #[serde(default)]
390    pub statistics_level: RocksDbStatisticsLevel,
391}
392
393impl RocksDbDatabaseInternal {
394    fn check_namespace(namespace: &str) -> Result<(), RocksDbStoreInternalError> {
395        if !namespace
396            .chars()
397            .all(|character| character.is_ascii_alphanumeric() || character == '_')
398        {
399            return Err(RocksDbStoreInternalError::InvalidNamespace);
400        }
401        Ok(())
402    }
403
404    fn build(
405        config: &RocksDbStoreInternalConfig,
406        namespace: &str,
407    ) -> Result<RocksDbDatabaseInternal, RocksDbStoreInternalError> {
408        let start_key = ROOT_KEY_DOMAIN.to_vec();
409        // Create a store to extract its executor and configuration
410        let temp_store = RocksDbStoreInternal::build(config, namespace, start_key)?;
411        Ok(RocksDbDatabaseInternal {
412            executor: temp_store.executor,
413            path_with_guard: temp_store.path_with_guard,
414            max_stream_queries: temp_store.max_stream_queries,
415            spawn_mode: temp_store.spawn_mode,
416        })
417    }
418}
419
420impl RocksDbStoreInternal {
421    fn build(
422        config: &RocksDbStoreInternalConfig,
423        namespace: &str,
424        start_key: Vec<u8>,
425    ) -> Result<RocksDbStoreInternal, RocksDbStoreInternalError> {
426        RocksDbDatabaseInternal::check_namespace(namespace)?;
427        let mut path_buf = config.path_with_guard.path_buf.clone();
428        let mut path_with_guard = config.path_with_guard.clone();
429        path_buf.push(namespace);
430        path_with_guard.path_buf = path_buf.clone();
431        let max_stream_queries = config.max_stream_queries;
432        let spawn_mode = config.spawn_mode;
433        if !std::path::Path::exists(&path_buf) {
434            std::fs::create_dir(path_buf.clone())?;
435        }
436        let sys = System::new_with_specifics(
437            RefreshKind::nothing().with_memory(MemoryRefreshKind::nothing().with_ram()),
438        );
439        let num_cpus = get_available_cpus();
440        let total_ram = get_available_memory(&sys);
441
442        let mut options = rocksdb::Options::default();
443        options.create_if_missing(true);
444        options.create_missing_column_families(true);
445
446        // Flush in-memory buffer to disk more often
447        options.set_write_buffer_size(WRITE_BUFFER_SIZE);
448        options.set_max_write_buffer_number(MAX_WRITE_BUFFER_NUMBER);
449        options.set_compression_type(rocksdb::DBCompressionType::Lz4);
450        options.set_level_zero_slowdown_writes_trigger(8);
451        options.set_level_zero_stop_writes_trigger(12);
452        options.set_level_zero_file_num_compaction_trigger(2);
453        // We deliberately give RocksDB one background thread *per* CPU so that
454        // flush + (N-1) compactions can hammer the NVMe at full bandwidth while
455        // still leaving enough CPU time for the foreground application threads.
456        options.increase_parallelism(num_cpus);
457        options.set_max_background_jobs(num_cpus);
458        options.set_max_subcompactions(num_cpus as u32);
459        options.set_level_compaction_dynamic_level_bytes(true);
460
461        options.set_compaction_style(DBCompactionStyle::Level);
462        options.set_target_file_size_base(2 * WRITE_BUFFER_SIZE as u64);
463
464        let mut block_options = BlockBasedOptions::default();
465        block_options.set_pin_l0_filter_and_index_blocks_in_cache(true);
466        block_options.set_cache_index_and_filter_blocks(true);
467        // Allocate 1/4 of total RAM for RocksDB block cache, which is a reasonable balance:
468        // - Large enough to significantly improve read performance by caching frequently accessed blocks
469        // - Small enough to leave memory for other system components
470        // - Follows common practice for database caching in server environments
471        // - Prevents excessive memory pressure that could lead to swapping or OOM conditions
472        block_options.set_block_cache(&Cache::new_hyper_clock_cache(
473            total_ram / 4,
474            HYPER_CLOCK_CACHE_BLOCK_SIZE,
475        ));
476
477        // Cap total memtable memory to prevent unbounded growth when multiple column
478        // families are used or many memtables accumulate before flushing.
479        let write_buffer_manager =
480            WriteBufferManager::new_write_buffer_manager(total_ram / 4, true);
481        options.set_write_buffer_manager(&write_buffer_manager);
482
483        // Configure bloom filters for prefix iteration optimization
484        block_options.set_bloom_filter(10.0, false);
485        block_options.set_whole_key_filtering(false);
486
487        // 32KB blocks instead of default 4KB - reduces iterator seeks
488        block_options.set_block_size(32 * 1024);
489        // Use latest format for better compression and performance
490        block_options.set_format_version(5);
491
492        options.set_block_based_table_factory(&block_options);
493
494        // Configure prefix extraction for bloom filter optimization
495        // Use 8 bytes: ROOT_KEY_DOMAIN (1 byte) + BCS variant (1-2 bytes) + identifier start (4-5 bytes)
496        let prefix_extractor = SliceTransform::create_fixed_prefix(8);
497        options.set_prefix_extractor(prefix_extractor);
498
499        // 12.5% of memtable size for bloom filter
500        options.set_memtable_prefix_bloom_ratio(0.125);
501        // Skip bloom filter for memtable when key exists
502        options.set_optimize_filters_for_hits(true);
503        // Use memory-mapped files for faster reads
504        options.set_allow_mmap_reads(true);
505        // Don't use random access pattern since we do prefix scans
506        options.set_advise_random_on_open(false);
507
508        if config.enable_statistics {
509            options.enable_statistics();
510            options.set_statistics_level(config.statistics_level.to_rocksdb());
511        }
512
513        let db = Arc::new(DB::open(&options, path_buf)?);
514        #[cfg(with_metrics)]
515        if config.enable_statistics {
516            statistics_metrics::register(Arc::new(options), db.clone());
517        }
518        let executor = RocksDbStoreExecutor { db, start_key };
519        Ok(RocksDbStoreInternal {
520            executor,
521            path_with_guard,
522            max_stream_queries,
523            spawn_mode,
524            root_key_written: Arc::new(AtomicBool::new(false)),
525        })
526    }
527}
528
529/// Exports RocksDB's internal statistics as Prometheus metrics.
530///
531/// The collector reads the values lazily at scrape time: cumulative tickers via
532/// `get_ticker_count` and instantaneous LSM state via `GetIntProperty`. Neither requires
533/// the (more expensive) histogram/timer statistics levels.
534#[cfg(with_metrics)]
535mod statistics_metrics {
536    use std::sync::{Arc, OnceLock};
537
538    use prometheus::{
539        core::{Collector, Desc},
540        proto::MetricFamily,
541        IntGauge,
542    };
543    use rocksdb::{statistics::Ticker, Options};
544
545    use super::DB;
546
547    enum Source {
548        Ticker(Ticker),
549        Property(&'static str),
550    }
551
552    struct Entry {
553        source: Source,
554        gauge: IntGauge,
555    }
556
557    fn definitions() -> Vec<(&'static str, &'static str, Source)> {
558        vec![
559            (
560                "linera_rocksdb_block_cache_hit",
561                "Cumulative RocksDB block cache hits since open",
562                Source::Ticker(Ticker::BlockCacheHit),
563            ),
564            (
565                "linera_rocksdb_block_cache_miss",
566                "Cumulative RocksDB block cache misses since open",
567                Source::Ticker(Ticker::BlockCacheMiss),
568            ),
569            (
570                "linera_rocksdb_compact_read_bytes",
571                "Cumulative bytes read during compaction since open",
572                Source::Ticker(Ticker::CompactReadBytes),
573            ),
574            (
575                "linera_rocksdb_compact_write_bytes",
576                "Cumulative bytes written during compaction since open",
577                Source::Ticker(Ticker::CompactWriteBytes),
578            ),
579            (
580                "linera_rocksdb_flush_write_bytes",
581                "Cumulative bytes written during flushes since open",
582                Source::Ticker(Ticker::FlushWriteBytes),
583            ),
584            (
585                "linera_rocksdb_stall_micros",
586                "Cumulative write-stall time in microseconds since open",
587                Source::Ticker(Ticker::StallMicros),
588            ),
589            (
590                "linera_rocksdb_bytes_written",
591                "Cumulative user bytes written since open",
592                Source::Ticker(Ticker::BytesWritten),
593            ),
594            (
595                "linera_rocksdb_bytes_read",
596                "Cumulative user bytes read since open",
597                Source::Ticker(Ticker::BytesRead),
598            ),
599            (
600                "linera_rocksdb_wal_bytes",
601                "Cumulative bytes written to the write-ahead log since open",
602                Source::Ticker(Ticker::WalFileBytes),
603            ),
604            (
605                "linera_rocksdb_bloom_filter_useful",
606                "Cumulative count of reads avoided by the bloom filter since open",
607                Source::Ticker(Ticker::BloomFilterUseful),
608            ),
609            (
610                "linera_rocksdb_memtable_hit",
611                "Cumulative memtable hits since open",
612                Source::Ticker(Ticker::MemtableHit),
613            ),
614            (
615                "linera_rocksdb_memtable_miss",
616                "Cumulative memtable misses since open",
617                Source::Ticker(Ticker::MemtableMiss),
618            ),
619            (
620                "linera_rocksdb_number_keys_written",
621                "Cumulative number of keys written since open",
622                Source::Ticker(Ticker::NumberKeysWritten),
623            ),
624            (
625                "linera_rocksdb_num_files_at_level0",
626                "Number of files at level 0",
627                Source::Property("rocksdb.num-files-at-level0"),
628            ),
629            (
630                "linera_rocksdb_estimate_pending_compaction_bytes",
631                "Estimated bytes pending compaction",
632                Source::Property("rocksdb.estimate-pending-compaction-bytes"),
633            ),
634            (
635                "linera_rocksdb_num_running_compactions",
636                "Number of currently running compactions",
637                Source::Property("rocksdb.num-running-compactions"),
638            ),
639            (
640                "linera_rocksdb_num_running_flushes",
641                "Number of currently running flushes",
642                Source::Property("rocksdb.num-running-flushes"),
643            ),
644            (
645                "linera_rocksdb_is_write_stopped",
646                "Whether writes are currently stopped (1) or not (0)",
647                Source::Property("rocksdb.is-write-stopped"),
648            ),
649            (
650                "linera_rocksdb_actual_delayed_write_rate",
651                "Current delayed write rate in bytes/s (0 when not delayed)",
652                Source::Property("rocksdb.actual-delayed-write-rate"),
653            ),
654            (
655                "linera_rocksdb_cur_size_all_mem_tables",
656                "Approximate size in bytes of all active and unflushed memtables",
657                Source::Property("rocksdb.cur-size-all-mem-tables"),
658            ),
659            (
660                "linera_rocksdb_num_immutable_mem_table",
661                "Number of immutable memtables not yet flushed",
662                Source::Property("rocksdb.num-immutable-mem-table"),
663            ),
664            (
665                "linera_rocksdb_live_sst_files_size",
666                "Total size in bytes of all live SST files",
667                Source::Property("rocksdb.live-sst-files-size"),
668            ),
669            (
670                "linera_rocksdb_total_sst_files_size",
671                "Total size in bytes of all SST files including obsolete ones",
672                Source::Property("rocksdb.total-sst-files-size"),
673            ),
674            (
675                "linera_rocksdb_estimate_num_keys",
676                "Estimated number of keys in the database",
677                Source::Property("rocksdb.estimate-num-keys"),
678            ),
679            (
680                "linera_rocksdb_block_cache_usage",
681                "Memory in bytes used by the block cache",
682                Source::Property("rocksdb.block-cache-usage"),
683            ),
684            (
685                "linera_rocksdb_block_cache_capacity",
686                "Capacity in bytes of the block cache",
687                Source::Property("rocksdb.block-cache-capacity"),
688            ),
689        ]
690    }
691
692    struct RocksDbStatisticsCollector {
693        options: Arc<Options>,
694        db: Arc<DB>,
695        entries: Vec<Entry>,
696    }
697
698    impl RocksDbStatisticsCollector {
699        fn new(options: Arc<Options>, db: Arc<DB>) -> Self {
700            let entries = definitions()
701                .into_iter()
702                .map(|(name, help, source)| Entry {
703                    source,
704                    gauge: IntGauge::new(name, help)
705                        .expect("RocksDB statistics metric name is valid"),
706                })
707                .collect();
708            Self {
709                options,
710                db,
711                entries,
712            }
713        }
714    }
715
716    impl Collector for RocksDbStatisticsCollector {
717        fn desc(&self) -> Vec<&Desc> {
718            self.entries
719                .iter()
720                .flat_map(|entry| entry.gauge.desc())
721                .collect()
722        }
723
724        fn collect(&self) -> Vec<MetricFamily> {
725            self.entries
726                .iter()
727                .flat_map(|entry| {
728                    let value = match &entry.source {
729                        Source::Ticker(ticker) => self.options.get_ticker_count(*ticker) as i64,
730                        Source::Property(property) => {
731                            self.db
732                                .property_int_value(*property)
733                                .ok()
734                                .flatten()
735                                .unwrap_or(0) as i64
736                        }
737                    };
738                    entry.gauge.set(value);
739                    entry.gauge.collect()
740                })
741                .collect()
742        }
743    }
744
745    pub(super) fn register(options: Arc<Options>, db: Arc<DB>) {
746        static REGISTERED: OnceLock<()> = OnceLock::new();
747        if REGISTERED.set(()).is_err() {
748            tracing::warn!(
749                "RocksDB statistics collector is already registered; skipping additional store"
750            );
751            return;
752        }
753        let collector = RocksDbStatisticsCollector::new(options, db);
754        if let Err(error) = prometheus::register(Box::new(collector)) {
755            tracing::warn!("failed to register the RocksDB statistics collector: {error}");
756        }
757    }
758
759    #[cfg(test)]
760    mod tests {
761        use std::collections::HashSet;
762
763        use super::{definitions, IntGauge};
764
765        #[test]
766        fn definitions_build_unique_valid_gauges() {
767            let definitions = definitions();
768            assert!(!definitions.is_empty());
769            let mut names = HashSet::new();
770            for (name, help, _source) in &definitions {
771                assert!(!help.is_empty(), "metric {name} has empty help text");
772                assert!(names.insert(*name), "duplicate metric name: {name}");
773                IntGauge::new(*name, *help).expect("metric definition should be valid");
774            }
775        }
776    }
777}
778
779impl WithError for RocksDbStoreInternal {
780    type Error = RocksDbStoreInternalError;
781}
782
783impl ReadableKeyValueStore for RocksDbStoreInternal {
784    const MAX_KEY_SIZE: usize = MAX_KEY_SIZE;
785
786    fn max_stream_queries(&self) -> usize {
787        self.max_stream_queries
788    }
789
790    fn root_key(&self) -> Result<Vec<u8>, RocksDbStoreInternalError> {
791        assert!(self.executor.start_key.starts_with(&ROOT_KEY_DOMAIN));
792        let root_key = self.executor.start_key[ROOT_KEY_DOMAIN.len()..].to_vec();
793        Ok(root_key)
794    }
795
796    async fn read_value_bytes(
797        &self,
798        key: &[u8],
799    ) -> Result<Option<Vec<u8>>, RocksDbStoreInternalError> {
800        check_key_size(key)?;
801        let db = self.executor.db.clone();
802        let mut full_key = self.executor.start_key.to_vec();
803        full_key.extend(key);
804        self.spawn_mode
805            .spawn(move |x| Ok(db.get(&x)?), full_key)
806            .await
807    }
808
809    async fn contains_key(&self, key: &[u8]) -> Result<bool, RocksDbStoreInternalError> {
810        check_key_size(key)?;
811        let db = self.executor.db.clone();
812        let mut full_key = self.executor.start_key.to_vec();
813        full_key.extend(key);
814        self.spawn_mode
815            .spawn(
816                move |x| {
817                    if !db.key_may_exist(&x) {
818                        return Ok(false);
819                    }
820                    Ok(db.get(&x)?.is_some())
821                },
822                full_key,
823            )
824            .await
825    }
826
827    async fn contains_keys(
828        &self,
829        keys: &[Vec<u8>],
830    ) -> Result<Vec<bool>, RocksDbStoreInternalError> {
831        let executor = self.executor.clone();
832        self.spawn_mode
833            .spawn(move |x| executor.contains_keys_internal(x), keys.to_vec())
834            .await
835    }
836
837    async fn read_multi_values_bytes(
838        &self,
839        keys: &[Vec<u8>],
840    ) -> Result<Vec<Option<Vec<u8>>>, RocksDbStoreInternalError> {
841        let executor = self.executor.clone();
842        self.spawn_mode
843            .spawn(
844                move |x| executor.read_multi_values_bytes_internal(x),
845                keys.to_vec(),
846            )
847            .await
848    }
849
850    async fn find_keys_by_prefix(
851        &self,
852        key_prefix: &[u8],
853    ) -> Result<Vec<Vec<u8>>, RocksDbStoreInternalError> {
854        let executor = self.executor.clone();
855        let key_prefix = key_prefix.to_vec();
856        self.spawn_mode
857            .spawn(
858                move |x| executor.find_keys_by_prefix_internal(x),
859                key_prefix,
860            )
861            .await
862    }
863
864    async fn find_key_values_by_prefix(
865        &self,
866        key_prefix: &[u8],
867    ) -> Result<Vec<(Vec<u8>, Vec<u8>)>, RocksDbStoreInternalError> {
868        let executor = self.executor.clone();
869        let key_prefix = key_prefix.to_vec();
870        self.spawn_mode
871            .spawn(
872                move |x| executor.find_key_values_by_prefix_internal(x),
873                key_prefix,
874            )
875            .await
876    }
877}
878
879impl WritableKeyValueStore for RocksDbStoreInternal {
880    const MAX_VALUE_SIZE: usize = MAX_VALUE_SIZE;
881
882    async fn write_batch(&self, batch: Batch) -> Result<(), RocksDbStoreInternalError> {
883        let write_root_key = !self.root_key_written.fetch_or(true, Ordering::SeqCst);
884        let executor = self.executor.clone();
885        self.spawn_mode
886            .spawn(
887                move |x| executor.write_batch_internal(x, write_root_key),
888                batch,
889            )
890            .await
891    }
892
893    async fn clear_journal(&self) -> Result<(), RocksDbStoreInternalError> {
894        Ok(())
895    }
896}
897
898impl KeyValueDatabase for RocksDbDatabaseInternal {
899    type Config = RocksDbStoreInternalConfig;
900    type Store = RocksDbStoreInternal;
901
902    fn get_name() -> String {
903        "rocksdb internal".to_string()
904    }
905
906    async fn connect(
907        config: &Self::Config,
908        namespace: &str,
909    ) -> Result<Self, RocksDbStoreInternalError> {
910        Self::build(config, namespace)
911    }
912
913    fn open_shared(&self, root_key: &[u8]) -> Result<Self::Store, RocksDbStoreInternalError> {
914        let mut start_key = ROOT_KEY_DOMAIN.to_vec();
915        start_key.extend(root_key);
916        let mut executor = self.executor.clone();
917        executor.start_key = start_key;
918        Ok(RocksDbStoreInternal {
919            executor,
920            path_with_guard: self.path_with_guard.clone(),
921            max_stream_queries: self.max_stream_queries,
922            spawn_mode: self.spawn_mode,
923            root_key_written: Arc::new(AtomicBool::new(false)),
924        })
925    }
926
927    fn open_exclusive(&self, root_key: &[u8]) -> Result<Self::Store, RocksDbStoreInternalError> {
928        self.open_shared(root_key)
929    }
930
931    async fn list_all(config: &Self::Config) -> Result<Vec<String>, RocksDbStoreInternalError> {
932        let entries = std::fs::read_dir(config.path_with_guard.path_buf.clone())?;
933        let mut namespaces = Vec::new();
934        for entry in entries {
935            let entry = entry?;
936            if !entry.file_type()?.is_dir() {
937                return Err(RocksDbStoreInternalError::NonDirectoryNamespace);
938            }
939            let namespace = match entry.file_name().into_string() {
940                Err(error) => {
941                    return Err(RocksDbStoreInternalError::IntoStringError(error));
942                }
943                Ok(namespace) => namespace,
944            };
945            namespaces.push(namespace);
946        }
947        Ok(namespaces)
948    }
949
950    async fn list_root_keys(&self) -> Result<Vec<Vec<u8>>, RocksDbStoreInternalError> {
951        let mut store = self.open_shared(&[])?;
952        store.executor.start_key = vec![STORED_ROOT_KEYS_PREFIX];
953        store.find_keys_by_prefix(&[]).await
954    }
955
956    async fn delete_all(config: &Self::Config) -> Result<(), RocksDbStoreInternalError> {
957        let namespaces = Self::list_all(config).await?;
958        for namespace in namespaces {
959            let mut path_buf = config.path_with_guard.path_buf.clone();
960            path_buf.push(&namespace);
961            std::fs::remove_dir_all(path_buf.as_path())?;
962        }
963        Ok(())
964    }
965
966    async fn exists(
967        config: &Self::Config,
968        namespace: &str,
969    ) -> Result<bool, RocksDbStoreInternalError> {
970        Self::check_namespace(namespace)?;
971        let mut path_buf = config.path_with_guard.path_buf.clone();
972        path_buf.push(namespace);
973        let test = std::path::Path::exists(&path_buf);
974        Ok(test)
975    }
976
977    async fn create(
978        config: &Self::Config,
979        namespace: &str,
980    ) -> Result<(), RocksDbStoreInternalError> {
981        Self::check_namespace(namespace)?;
982        let mut path_buf = config.path_with_guard.path_buf.clone();
983        path_buf.push(namespace);
984        if std::path::Path::exists(&path_buf) {
985            return Err(RocksDbStoreInternalError::StoreAlreadyExists);
986        }
987        std::fs::create_dir_all(path_buf)?;
988        Ok(())
989    }
990
991    async fn delete(
992        config: &Self::Config,
993        namespace: &str,
994    ) -> Result<(), RocksDbStoreInternalError> {
995        Self::check_namespace(namespace)?;
996        let mut path_buf = config.path_with_guard.path_buf.clone();
997        path_buf.push(namespace);
998        let path = path_buf.as_path();
999        std::fs::remove_dir_all(path)?;
1000        Ok(())
1001    }
1002}
1003
1004#[cfg(with_testing)]
1005impl TestKeyValueDatabase for RocksDbDatabaseInternal {
1006    async fn new_test_config() -> Result<RocksDbStoreInternalConfig, RocksDbStoreInternalError> {
1007        let path_with_guard = PathWithGuard::new_testing();
1008        let spawn_mode = RocksDbSpawnMode::get_spawn_mode_from_runtime();
1009        let max_stream_queries = TEST_ROCKS_DB_MAX_STREAM_QUERIES;
1010        Ok(RocksDbStoreInternalConfig {
1011            path_with_guard,
1012            spawn_mode,
1013            max_stream_queries,
1014            enable_statistics: false,
1015            statistics_level: RocksDbStatisticsLevel::default(),
1016        })
1017    }
1018}
1019
1020/// The error type for [`RocksDbStoreInternal`]
1021#[derive(Error, Debug)]
1022pub enum RocksDbStoreInternalError {
1023    /// Store already exists
1024    #[error("Store already exists")]
1025    StoreAlreadyExists,
1026
1027    /// Tokio join error in RocksDB.
1028    #[error("tokio join error: {0}")]
1029    TokioJoinError(#[from] tokio::task::JoinError),
1030
1031    /// RocksDB error.
1032    #[error("RocksDB error: {0}")]
1033    RocksDb(#[from] rocksdb::Error),
1034
1035    /// The database contains a file which is not a directory
1036    #[error("Namespaces should be directories")]
1037    NonDirectoryNamespace,
1038
1039    /// Error converting `OsString` to `String`
1040    #[error("error in the conversion from OsString: {0:?}")]
1041    IntoStringError(OsString),
1042
1043    /// The key must have at most 8 MiB
1044    #[error("The key must have at most 8 MiB")]
1045    KeyTooLong,
1046
1047    /// Namespace contains forbidden characters
1048    #[error("Namespace contains forbidden characters")]
1049    InvalidNamespace,
1050
1051    /// Filesystem error
1052    #[error("Filesystem error: {0}")]
1053    FsError(#[from] std::io::Error),
1054
1055    /// BCS serialization error.
1056    #[error(transparent)]
1057    BcsError(#[from] bcs::Error),
1058}
1059
1060/// A path and the guard for the temporary directory if needed
1061#[derive(Clone, Debug, Deserialize, Serialize)]
1062pub struct PathWithGuard {
1063    /// The path to the data
1064    pub path_buf: PathBuf,
1065    /// The guard for the directory if one is needed
1066    #[serde(skip)]
1067    _dir: Option<Arc<TempDir>>,
1068}
1069
1070impl PathWithGuard {
1071    /// Creates a `PathWithGuard` from an existing path.
1072    pub fn new(path_buf: PathBuf) -> Self {
1073        Self {
1074            path_buf,
1075            _dir: None,
1076        }
1077    }
1078
1079    /// Returns the test path for RocksDB without common config.
1080    #[cfg(with_testing)]
1081    fn new_testing() -> PathWithGuard {
1082        let dir = TempDir::new().unwrap();
1083        let path_buf = dir.path().to_path_buf();
1084        let dir_guard = Some(Arc::new(dir));
1085        PathWithGuard {
1086            path_buf,
1087            _dir: dir_guard,
1088        }
1089    }
1090}
1091
1092impl PartialEq for PathWithGuard {
1093    fn eq(&self, other: &Self) -> bool {
1094        self.path_buf == other.path_buf
1095    }
1096}
1097impl Eq for PathWithGuard {}
1098
1099impl KeyValueStoreError for RocksDbStoreInternalError {
1100    const BACKEND: &'static str = "rocks_db";
1101}
1102
1103/// The composed error type for the `RocksDbStore`
1104pub type RocksDbStoreError = ValueSplittingError<RocksDbStoreInternalError>;
1105
1106/// The composed config type for the `RocksDbStore`
1107pub type RocksDbStoreConfig = LruCachingConfig<RocksDbStoreInternalConfig>;
1108
1109/// The `RocksDbDatabase` composed type with metrics
1110#[cfg(with_metrics)]
1111pub type RocksDbDatabase = MeteredDatabase<
1112    LruCachingDatabase<
1113        MeteredDatabase<ValueSplittingDatabase<MeteredDatabase<RocksDbDatabaseInternal>>>,
1114    >,
1115>;
1116/// The `RocksDbDatabase` composed type
1117#[cfg(not(with_metrics))]
1118pub type RocksDbDatabase = LruCachingDatabase<ValueSplittingDatabase<RocksDbDatabaseInternal>>;