zebra_state/service/finalized_state/disk_db.rs
1//! Provides low-level access to RocksDB using some database-specific types.
2//!
3//! This module makes sure that:
4//! - all disk writes happen inside a RocksDB transaction
5//! ([`rocksdb::WriteBatch`]), and
6//! - format-specific invariants are maintained.
7//!
8//! # Correctness
9//!
10//! [`crate::constants::state_database_format_version_in_code()`] must be incremented
11//! each time the database format (column, serialization, etc) changes.
12
13use std::{
14 collections::{BTreeMap, HashMap},
15 fmt::{Debug, Write},
16 fs,
17 ops::RangeBounds,
18 path::Path,
19 sync::{
20 atomic::{self, AtomicBool},
21 Arc,
22 },
23};
24
25use itertools::Itertools;
26use rlimit::increase_nofile_limit;
27
28use rocksdb::{ColumnFamilyDescriptor, ErrorKind, Options, ReadOptions};
29use semver::Version;
30use zebra_chain::{parameters::Network, primitives::byte_array::increment_big_endian};
31
32use crate::{
33 database_format_version_on_disk,
34 service::finalized_state::disk_format::{FromDisk, IntoDisk},
35 write_database_format_version_to_disk, Config, StateInitError,
36};
37
38use super::zebra_db::transparent::{
39 fetch_add_balance_and_received, BALANCE_BY_TRANSPARENT_ADDR,
40 BALANCE_BY_TRANSPARENT_ADDR_MERGE_OP,
41};
42// Doc-only imports
43#[allow(unused_imports)]
44use super::{TypedColumnFamily, WriteTypedBatch};
45
46#[cfg(any(test, feature = "proptest-impl"))]
47mod tests;
48
49/// The [`rocksdb::ThreadMode`] used by the database.
50pub type DBThreadMode = rocksdb::SingleThreaded;
51
52/// The [`rocksdb`] database type, including thread mode.
53///
54/// Also the [`rocksdb::DBAccess`] used by database iterators.
55pub type DB = rocksdb::DBWithThreadMode<DBThreadMode>;
56
57/// Wrapper struct to ensure low-level database access goes through the correct API.
58///
59/// `rocksdb` allows concurrent writes through a shared reference,
60/// so database instances are cloneable. When the final clone is dropped,
61/// the database is closed.
62///
63/// # Correctness
64///
65/// Reading transactions from the database using RocksDB iterators causes hangs.
66/// But creating iterators and reading the tip height works fine.
67///
68/// So these hangs are probably caused by holding column family locks to read:
69/// - multiple values, or
70/// - large values.
71///
72/// This bug might be fixed by moving database operations to blocking threads (#2188),
73/// so that they don't block the tokio executor.
74/// (Or it might be fixed by future RocksDB upgrades.)
75#[derive(Clone, Debug)]
76pub struct DiskDb {
77 // Configuration
78 //
79 // This configuration cannot be modified after the database is initialized,
80 // because some clones would have different values.
81 //
82 /// The configured database kind for this database.
83 db_kind: String,
84
85 /// The format version of the running Zebra code.
86 format_version_in_code: Version,
87
88 /// The configured network for this database.
89 network: Network,
90
91 /// How this database was opened: a read-write primary (persistent or ephemeral)
92 /// or a read-only secondary. See [`DbMode`].
93 mode: DbMode,
94
95 /// A boolean flag indicating whether the db format change task has finished
96 /// applying any format changes that may have been required.
97 finished_format_upgrades: Arc<AtomicBool>,
98
99 // Owned State
100 //
101 // Everything contained in this state must be shared by all clones, or read-only.
102 //
103 /// The shared inner RocksDB database.
104 ///
105 /// RocksDB allows reads and writes via a shared reference.
106 ///
107 /// In [`SingleThreaded`](rocksdb::SingleThreaded) mode,
108 /// column family changes and [`Drop`] require exclusive access.
109 ///
110 /// In [`MultiThreaded`](rocksdb::MultiThreaded) mode,
111 /// only [`Drop`] requires exclusive access.
112 db: Arc<DB>,
113}
114
115/// Wrapper struct to ensure low-level database writes go through the correct API.
116///
117/// [`rocksdb::WriteBatch`] is a batched set of database updates,
118/// which must be written to the database using `DiskDb::write(batch)`.
119#[must_use = "batches must be written to the database"]
120#[derive(Default)]
121pub struct DiskWriteBatch {
122 /// The inner RocksDB write batch.
123 batch: rocksdb::WriteBatch,
124}
125
126impl Debug for DiskWriteBatch {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 f.debug_struct("DiskWriteBatch")
129 .field("batch", &format!("{} bytes", self.batch.size_in_bytes()))
130 .finish()
131 }
132}
133
134impl PartialEq for DiskWriteBatch {
135 fn eq(&self, other: &Self) -> bool {
136 self.batch.data() == other.batch.data()
137 }
138}
139
140impl Eq for DiskWriteBatch {}
141
142/// Helper trait for inserting serialized typed (Key, Value) pairs into rocksdb.
143///
144/// # Deprecation
145///
146/// This trait should not be used in new code, use [`WriteTypedBatch`] instead.
147//
148// TODO: replace uses of this trait with WriteTypedBatch,
149// implement these methods directly on WriteTypedBatch, and delete the trait.
150pub trait WriteDisk {
151 /// Serialize and insert the given key and value into a rocksdb column family,
152 /// overwriting any existing `value` for `key`.
153 fn zs_insert<C, K, V>(&mut self, cf: &C, key: K, value: V)
154 where
155 C: rocksdb::AsColumnFamilyRef,
156 K: IntoDisk + Debug,
157 V: IntoDisk;
158
159 /// Serialize and merge the given key and value into a rocksdb column family,
160 /// merging with any existing `value` for `key`.
161 fn zs_merge<C, K, V>(&mut self, cf: &C, key: K, value: V)
162 where
163 C: rocksdb::AsColumnFamilyRef,
164 K: IntoDisk + Debug,
165 V: IntoDisk;
166
167 /// Remove the given key from a rocksdb column family, if it exists.
168 fn zs_delete<C, K>(&mut self, cf: &C, key: K)
169 where
170 C: rocksdb::AsColumnFamilyRef,
171 K: IntoDisk + Debug;
172
173 /// Delete the given key range from a rocksdb column family, if it exists, including `from`
174 /// and excluding `until_strictly_before`.
175 //
176 // TODO: convert zs_delete_range() to take std::ops::RangeBounds
177 // see zs_range_iter() for an example of the edge cases
178 fn zs_delete_range<C, K>(&mut self, cf: &C, from: K, until_strictly_before: K)
179 where
180 C: rocksdb::AsColumnFamilyRef,
181 K: IntoDisk + Debug;
182}
183
184/// # Deprecation
185///
186/// These impls should not be used in new code, use [`WriteTypedBatch`] instead.
187//
188// TODO: replace uses of these impls with WriteTypedBatch,
189// implement these methods directly on WriteTypedBatch, and delete the trait.
190impl WriteDisk for DiskWriteBatch {
191 fn zs_insert<C, K, V>(&mut self, cf: &C, key: K, value: V)
192 where
193 C: rocksdb::AsColumnFamilyRef,
194 K: IntoDisk + Debug,
195 V: IntoDisk,
196 {
197 let key_bytes = key.as_bytes();
198 let value_bytes = value.as_bytes();
199 self.batch.put_cf(cf, key_bytes, value_bytes);
200 }
201
202 fn zs_merge<C, K, V>(&mut self, cf: &C, key: K, value: V)
203 where
204 C: rocksdb::AsColumnFamilyRef,
205 K: IntoDisk + Debug,
206 V: IntoDisk,
207 {
208 let key_bytes = key.as_bytes();
209 let value_bytes = value.as_bytes();
210 self.batch.merge_cf(cf, key_bytes, value_bytes);
211 }
212
213 fn zs_delete<C, K>(&mut self, cf: &C, key: K)
214 where
215 C: rocksdb::AsColumnFamilyRef,
216 K: IntoDisk + Debug,
217 {
218 let key_bytes = key.as_bytes();
219 self.batch.delete_cf(cf, key_bytes);
220 }
221
222 // TODO: convert zs_delete_range() to take std::ops::RangeBounds
223 // see zs_range_iter() for an example of the edge cases
224 fn zs_delete_range<C, K>(&mut self, cf: &C, from: K, until_strictly_before: K)
225 where
226 C: rocksdb::AsColumnFamilyRef,
227 K: IntoDisk + Debug,
228 {
229 let from_bytes = from.as_bytes();
230 let until_strictly_before_bytes = until_strictly_before.as_bytes();
231 self.batch
232 .delete_range_cf(cf, from_bytes, until_strictly_before_bytes);
233 }
234}
235
236// Allow &mut DiskWriteBatch as well as owned DiskWriteBatch
237impl<T> WriteDisk for &mut T
238where
239 T: WriteDisk,
240{
241 fn zs_insert<C, K, V>(&mut self, cf: &C, key: K, value: V)
242 where
243 C: rocksdb::AsColumnFamilyRef,
244 K: IntoDisk + Debug,
245 V: IntoDisk,
246 {
247 (*self).zs_insert(cf, key, value)
248 }
249
250 fn zs_merge<C, K, V>(&mut self, cf: &C, key: K, value: V)
251 where
252 C: rocksdb::AsColumnFamilyRef,
253 K: IntoDisk + Debug,
254 V: IntoDisk,
255 {
256 (*self).zs_merge(cf, key, value)
257 }
258
259 fn zs_delete<C, K>(&mut self, cf: &C, key: K)
260 where
261 C: rocksdb::AsColumnFamilyRef,
262 K: IntoDisk + Debug,
263 {
264 (*self).zs_delete(cf, key)
265 }
266
267 fn zs_delete_range<C, K>(&mut self, cf: &C, from: K, until_strictly_before: K)
268 where
269 C: rocksdb::AsColumnFamilyRef,
270 K: IntoDisk + Debug,
271 {
272 (*self).zs_delete_range(cf, from, until_strictly_before)
273 }
274}
275
276/// Helper trait for retrieving and deserializing values from rocksdb column families.
277///
278/// # Deprecation
279///
280/// This trait should not be used in new code, use [`TypedColumnFamily`] instead.
281//
282// TODO: replace uses of this trait with TypedColumnFamily,
283// implement these methods directly on DiskDb, and delete the trait.
284pub trait ReadDisk {
285 /// Returns true if a rocksdb column family `cf` does not contain any entries.
286 fn zs_is_empty<C>(&self, cf: &C) -> bool
287 where
288 C: rocksdb::AsColumnFamilyRef;
289
290 /// Returns the value for `key` in the rocksdb column family `cf`, if present.
291 fn zs_get<C, K, V>(&self, cf: &C, key: &K) -> Option<V>
292 where
293 C: rocksdb::AsColumnFamilyRef,
294 K: IntoDisk,
295 V: FromDisk;
296
297 /// Check if a rocksdb column family `cf` contains the serialized form of `key`.
298 fn zs_contains<C, K>(&self, cf: &C, key: &K) -> bool
299 where
300 C: rocksdb::AsColumnFamilyRef,
301 K: IntoDisk;
302
303 /// Returns the lowest key in `cf`, and the corresponding value.
304 ///
305 /// Returns `None` if the column family is empty.
306 fn zs_first_key_value<C, K, V>(&self, cf: &C) -> Option<(K, V)>
307 where
308 C: rocksdb::AsColumnFamilyRef,
309 K: IntoDisk + FromDisk,
310 V: FromDisk;
311
312 /// Returns the highest key in `cf`, and the corresponding value.
313 ///
314 /// Returns `None` if the column family is empty.
315 fn zs_last_key_value<C, K, V>(&self, cf: &C) -> Option<(K, V)>
316 where
317 C: rocksdb::AsColumnFamilyRef,
318 K: IntoDisk + FromDisk,
319 V: FromDisk;
320
321 /// Returns the first key greater than or equal to `lower_bound` in `cf`,
322 /// and the corresponding value.
323 ///
324 /// Returns `None` if there are no keys greater than or equal to `lower_bound`.
325 fn zs_next_key_value_from<C, K, V>(&self, cf: &C, lower_bound: &K) -> Option<(K, V)>
326 where
327 C: rocksdb::AsColumnFamilyRef,
328 K: IntoDisk + FromDisk,
329 V: FromDisk;
330
331 /// Returns the first key strictly greater than `lower_bound` in `cf`,
332 /// and the corresponding value.
333 ///
334 /// Returns `None` if there are no keys greater than `lower_bound`.
335 fn zs_next_key_value_strictly_after<C, K, V>(&self, cf: &C, lower_bound: &K) -> Option<(K, V)>
336 where
337 C: rocksdb::AsColumnFamilyRef,
338 K: IntoDisk + FromDisk,
339 V: FromDisk;
340
341 /// Returns the first key less than or equal to `upper_bound` in `cf`,
342 /// and the corresponding value.
343 ///
344 /// Returns `None` if there are no keys less than or equal to `upper_bound`.
345 fn zs_prev_key_value_back_from<C, K, V>(&self, cf: &C, upper_bound: &K) -> Option<(K, V)>
346 where
347 C: rocksdb::AsColumnFamilyRef,
348 K: IntoDisk + FromDisk,
349 V: FromDisk;
350
351 /// Returns the first key strictly less than `upper_bound` in `cf`,
352 /// and the corresponding value.
353 ///
354 /// Returns `None` if there are no keys less than `upper_bound`.
355 fn zs_prev_key_value_strictly_before<C, K, V>(&self, cf: &C, upper_bound: &K) -> Option<(K, V)>
356 where
357 C: rocksdb::AsColumnFamilyRef,
358 K: IntoDisk + FromDisk,
359 V: FromDisk;
360
361 /// Returns the keys and values in `cf` in `range`, in an ordered `BTreeMap`.
362 ///
363 /// Holding this iterator open might delay block commit transactions.
364 fn zs_items_in_range_ordered<C, K, V, R>(&self, cf: &C, range: R) -> BTreeMap<K, V>
365 where
366 C: rocksdb::AsColumnFamilyRef,
367 K: IntoDisk + FromDisk + Ord,
368 V: FromDisk,
369 R: RangeBounds<K>;
370
371 /// Returns the keys and values in `cf` in `range`, in an unordered `HashMap`.
372 ///
373 /// Holding this iterator open might delay block commit transactions.
374 fn zs_items_in_range_unordered<C, K, V, R>(&self, cf: &C, range: R) -> HashMap<K, V>
375 where
376 C: rocksdb::AsColumnFamilyRef,
377 K: IntoDisk + FromDisk + Eq + std::hash::Hash,
378 V: FromDisk,
379 R: RangeBounds<K>;
380}
381
382/// How a [`DiskDb`] instance is opened.
383///
384/// These modes are mutually exclusive — an ephemeral database is a read-write primary
385/// this process owns and deletes on drop, a persistent database is a read-write primary
386/// whose files are kept, and a read-only secondary follows another process's primary and
387/// never writes, flushes, or deletes it.
388#[derive(Clone, Copy, Debug, PartialEq, Eq)]
389enum DbMode {
390 /// A read-write primary backed by persistent on-disk files that are kept on drop.
391 Persistent,
392
393 /// A read-write primary backed by temporary files that are deleted on drop.
394 Ephemeral,
395
396 /// A read-only secondary that follows an existing primary's on-disk state. It owns
397 /// no files and never writes, flushes, or deletes them.
398 ReadOnlySecondary,
399}
400
401impl DbMode {
402 /// Returns true if dropping the database should delete its files.
403 fn deletes_files_on_drop(self) -> bool {
404 matches!(self, DbMode::Ephemeral)
405 }
406
407 /// Returns true if this is a read-only secondary instance, which RocksDB does not
408 /// allow writes or flushes on.
409 fn is_read_only(self) -> bool {
410 matches!(self, DbMode::ReadOnlySecondary)
411 }
412}
413
414impl PartialEq for DiskDb {
415 fn eq(&self, other: &Self) -> bool {
416 if self.db.path() == other.db.path() {
417 assert_eq!(
418 self.network, other.network,
419 "database with same path but different network configs",
420 );
421 assert_eq!(
422 self.mode.deletes_files_on_drop(),
423 other.mode.deletes_files_on_drop(),
424 "database with same path but different ephemeral configs",
425 );
426
427 return true;
428 }
429
430 false
431 }
432}
433
434impl Eq for DiskDb {}
435
436/// # Deprecation
437///
438/// These impls should not be used in new code, use [`TypedColumnFamily`] instead.
439//
440// TODO: replace uses of these impls with TypedColumnFamily,
441// implement these methods directly on DiskDb, and delete the trait.
442impl ReadDisk for DiskDb {
443 fn zs_is_empty<C>(&self, cf: &C) -> bool
444 where
445 C: rocksdb::AsColumnFamilyRef,
446 {
447 // Empty column families return invalid forward iterators.
448 //
449 // Checking iterator validity does not seem to cause database hangs.
450 let iterator = self.db.iterator_cf(cf, rocksdb::IteratorMode::Start);
451 let raw_iterator: rocksdb::DBRawIteratorWithThreadMode<DB> = iterator.into();
452
453 !raw_iterator.valid()
454 }
455
456 #[allow(clippy::unwrap_in_result)]
457 fn zs_get<C, K, V>(&self, cf: &C, key: &K) -> Option<V>
458 where
459 C: rocksdb::AsColumnFamilyRef,
460 K: IntoDisk,
461 V: FromDisk,
462 {
463 let key_bytes = key.as_bytes();
464
465 // We use `get_pinned_cf` to avoid taking ownership of the serialized
466 // value, because we're going to deserialize it anyways, which avoids an
467 // extra copy
468 let value_bytes = self
469 .db
470 .get_pinned_cf(cf, key_bytes)
471 .expect("unexpected database failure");
472
473 value_bytes.map(V::from_bytes)
474 }
475
476 fn zs_contains<C, K>(&self, cf: &C, key: &K) -> bool
477 where
478 C: rocksdb::AsColumnFamilyRef,
479 K: IntoDisk,
480 {
481 let key_bytes = key.as_bytes();
482
483 // We use `get_pinned_cf` to avoid taking ownership of the serialized
484 // value, because we don't use the value at all. This avoids an extra copy.
485 self.db
486 .get_pinned_cf(cf, key_bytes)
487 .expect("unexpected database failure")
488 .is_some()
489 }
490
491 fn zs_first_key_value<C, K, V>(&self, cf: &C) -> Option<(K, V)>
492 where
493 C: rocksdb::AsColumnFamilyRef,
494 K: IntoDisk + FromDisk,
495 V: FromDisk,
496 {
497 // Reading individual values from iterators does not seem to cause database hangs.
498 self.zs_forward_range_iter(cf, ..).next()
499 }
500
501 fn zs_last_key_value<C, K, V>(&self, cf: &C) -> Option<(K, V)>
502 where
503 C: rocksdb::AsColumnFamilyRef,
504 K: IntoDisk + FromDisk,
505 V: FromDisk,
506 {
507 // Reading individual values from iterators does not seem to cause database hangs.
508 self.zs_reverse_range_iter(cf, ..).next()
509 }
510
511 fn zs_next_key_value_from<C, K, V>(&self, cf: &C, lower_bound: &K) -> Option<(K, V)>
512 where
513 C: rocksdb::AsColumnFamilyRef,
514 K: IntoDisk + FromDisk,
515 V: FromDisk,
516 {
517 self.zs_forward_range_iter(cf, lower_bound..).next()
518 }
519
520 fn zs_next_key_value_strictly_after<C, K, V>(&self, cf: &C, lower_bound: &K) -> Option<(K, V)>
521 where
522 C: rocksdb::AsColumnFamilyRef,
523 K: IntoDisk + FromDisk,
524 V: FromDisk,
525 {
526 use std::ops::Bound::*;
527
528 // There is no standard syntax for an excluded start bound.
529 self.zs_forward_range_iter(cf, (Excluded(lower_bound), Unbounded))
530 .next()
531 }
532
533 fn zs_prev_key_value_back_from<C, K, V>(&self, cf: &C, upper_bound: &K) -> Option<(K, V)>
534 where
535 C: rocksdb::AsColumnFamilyRef,
536 K: IntoDisk + FromDisk,
537 V: FromDisk,
538 {
539 self.zs_reverse_range_iter(cf, ..=upper_bound).next()
540 }
541
542 fn zs_prev_key_value_strictly_before<C, K, V>(&self, cf: &C, upper_bound: &K) -> Option<(K, V)>
543 where
544 C: rocksdb::AsColumnFamilyRef,
545 K: IntoDisk + FromDisk,
546 V: FromDisk,
547 {
548 self.zs_reverse_range_iter(cf, ..upper_bound).next()
549 }
550
551 fn zs_items_in_range_ordered<C, K, V, R>(&self, cf: &C, range: R) -> BTreeMap<K, V>
552 where
553 C: rocksdb::AsColumnFamilyRef,
554 K: IntoDisk + FromDisk + Ord,
555 V: FromDisk,
556 R: RangeBounds<K>,
557 {
558 self.zs_forward_range_iter(cf, range).collect()
559 }
560
561 fn zs_items_in_range_unordered<C, K, V, R>(&self, cf: &C, range: R) -> HashMap<K, V>
562 where
563 C: rocksdb::AsColumnFamilyRef,
564 K: IntoDisk + FromDisk + Eq + std::hash::Hash,
565 V: FromDisk,
566 R: RangeBounds<K>,
567 {
568 self.zs_forward_range_iter(cf, range).collect()
569 }
570}
571
572impl DiskWriteBatch {
573 /// Creates and returns a new transactional batch write.
574 ///
575 /// # Correctness
576 ///
577 /// Each block must be written to the state inside a batch, so that:
578 /// - concurrent `ReadStateService` queries don't see half-written blocks, and
579 /// - if Zebra calls `exit`, panics, or crashes, half-written blocks are rolled back.
580 pub fn new() -> Self {
581 DiskWriteBatch {
582 batch: rocksdb::WriteBatch::default(),
583 }
584 }
585}
586
587impl DiskDb {
588 /// Prints rocksdb metrics for each column family along with total database disk size, live data disk size and database memory size.
589 pub fn print_db_metrics(&self) {
590 let mut total_size_on_disk = 0;
591 let mut total_live_size_on_disk = 0;
592 let mut total_size_in_mem = 0;
593 let db: &Arc<DB> = &self.db;
594 let db_options = DiskDb::options();
595 let column_families = DiskDb::construct_column_families(db_options, db.path(), []);
596 let mut column_families_log_string = String::from("");
597
598 write!(column_families_log_string, "Column families and sizes: ").unwrap();
599
600 for cf_descriptor in column_families {
601 let cf_name = &cf_descriptor.name();
602 let cf_handle = db
603 .cf_handle(cf_name)
604 .expect("Column family handle must exist");
605 let live_data_size = db
606 .property_int_value_cf(cf_handle, "rocksdb.estimate-live-data-size")
607 .unwrap_or(Some(0));
608 let total_sst_files_size = db
609 .property_int_value_cf(cf_handle, "rocksdb.total-sst-files-size")
610 .unwrap_or(Some(0));
611 let cf_disk_size = total_sst_files_size.unwrap_or(0);
612 total_size_on_disk += cf_disk_size;
613 total_live_size_on_disk += live_data_size.unwrap_or(0);
614 let mem_table_size = db
615 .property_int_value_cf(cf_handle, "rocksdb.size-all-mem-tables")
616 .unwrap_or(Some(0));
617 total_size_in_mem += mem_table_size.unwrap_or(0);
618
619 write!(
620 column_families_log_string,
621 "{} (Disk: {}, Memory: {})",
622 cf_name,
623 human_bytes::human_bytes(cf_disk_size as f64),
624 human_bytes::human_bytes(mem_table_size.unwrap_or(0) as f64)
625 )
626 .unwrap();
627 }
628
629 debug!("{}", column_families_log_string);
630 info!(
631 "Total Database Disk Size: {}",
632 human_bytes::human_bytes(total_size_on_disk as f64)
633 );
634 info!(
635 "Total Live Data Disk Size: {}",
636 human_bytes::human_bytes(total_live_size_on_disk as f64)
637 );
638 info!(
639 "Total Database Memory Size: {}",
640 human_bytes::human_bytes(total_size_in_mem as f64)
641 );
642 }
643
644 /// Exports RocksDB metrics to Prometheus.
645 ///
646 /// This function collects database statistics and exposes them as Prometheus metrics.
647 /// Call this periodically (e.g., every 30 seconds) from a background task.
648 pub(crate) fn export_metrics(&self) {
649 let db: &Arc<DB> = &self.db;
650 let db_options = DiskDb::options();
651 let column_families = DiskDb::construct_column_families(db_options, db.path(), []);
652
653 let mut total_disk: u64 = 0;
654 let mut total_live: u64 = 0;
655 let mut total_mem: u64 = 0;
656
657 for cf_descriptor in column_families {
658 let cf_name = cf_descriptor.name().to_string();
659 if let Some(cf_handle) = db.cf_handle(&cf_name) {
660 let disk = db
661 .property_int_value_cf(cf_handle, "rocksdb.total-sst-files-size")
662 .ok()
663 .flatten()
664 .unwrap_or(0);
665 let live = db
666 .property_int_value_cf(cf_handle, "rocksdb.estimate-live-data-size")
667 .ok()
668 .flatten()
669 .unwrap_or(0);
670 let mem = db
671 .property_int_value_cf(cf_handle, "rocksdb.size-all-mem-tables")
672 .ok()
673 .flatten()
674 .unwrap_or(0);
675
676 total_disk += disk;
677 total_live += live;
678 total_mem += mem;
679
680 metrics::gauge!("zebra.state.rocksdb.cf_disk_size_bytes", "cf" => cf_name.clone())
681 .set(disk as f64);
682 metrics::gauge!("zebra.state.rocksdb.cf_memory_size_bytes", "cf" => cf_name)
683 .set(mem as f64);
684 }
685 }
686
687 metrics::gauge!("zebra.state.rocksdb.total_disk_size_bytes").set(total_disk as f64);
688 metrics::gauge!("zebra.state.rocksdb.live_data_size_bytes").set(total_live as f64);
689 metrics::gauge!("zebra.state.rocksdb.total_memory_size_bytes").set(total_mem as f64);
690
691 // Compaction metrics - these use database-wide properties (not per-column-family)
692 if let Ok(Some(pending)) = db.property_int_value("rocksdb.compaction-pending") {
693 metrics::gauge!("zebra.state.rocksdb.compaction.pending_bytes").set(pending as f64);
694 }
695
696 if let Ok(Some(running)) = db.property_int_value("rocksdb.num-running-compactions") {
697 metrics::gauge!("zebra.state.rocksdb.compaction.running").set(running as f64);
698 }
699
700 if let Ok(Some(cache)) = db.property_int_value("rocksdb.block-cache-usage") {
701 metrics::gauge!("zebra.state.rocksdb.block_cache_usage_bytes").set(cache as f64);
702 }
703
704 // Level-by-level file counts (RocksDB typically has up to 7 levels)
705 for level in 0..7 {
706 let prop = format!("rocksdb.num-files-at-level{level}");
707 if let Ok(Some(count)) = db.property_int_value(&prop) {
708 metrics::gauge!("zebra.state.rocksdb.num_files_at_level", "level" => level.to_string())
709 .set(count as f64);
710 }
711 }
712 }
713
714 /// Returns the estimated total disk space usage of the database.
715 pub fn size(&self) -> u64 {
716 let db: &Arc<DB> = &self.db;
717 let db_options = DiskDb::options();
718 let mut total_size_on_disk = 0;
719 for cf_descriptor in DiskDb::construct_column_families(db_options, db.path(), []) {
720 let cf_name = &cf_descriptor.name();
721 let cf_handle = db
722 .cf_handle(cf_name)
723 .expect("Column family handle must exist");
724
725 total_size_on_disk += db
726 .property_int_value_cf(cf_handle, "rocksdb.total-sst-files-size")
727 .ok()
728 .flatten()
729 .unwrap_or(0);
730 }
731
732 total_size_on_disk
733 }
734
735 /// Sets `finished_format_upgrades` to true to indicate that Zebra has
736 /// finished applying any required db format upgrades.
737 pub fn mark_finished_format_upgrades(&self) {
738 self.finished_format_upgrades
739 .store(true, atomic::Ordering::SeqCst);
740 }
741
742 /// Returns true if the `finished_format_upgrades` flag has been set to true to
743 /// indicate that Zebra has finished applying any required db format upgrades.
744 pub fn finished_format_upgrades(&self) -> bool {
745 self.finished_format_upgrades.load(atomic::Ordering::SeqCst)
746 }
747
748 /// When called with a secondary DB instance, tries to catch up with the primary DB instance
749 pub fn try_catch_up_with_primary(&self) -> Result<(), rocksdb::Error> {
750 self.db.try_catch_up_with_primary()
751 }
752
753 /// Returns a forward iterator over the items in `cf` in `range`.
754 ///
755 /// Holding this iterator open might delay block commit transactions.
756 pub fn zs_forward_range_iter<C, K, V, R>(
757 &self,
758 cf: &C,
759 range: R,
760 ) -> impl Iterator<Item = (K, V)> + '_
761 where
762 C: rocksdb::AsColumnFamilyRef,
763 K: IntoDisk + FromDisk,
764 V: FromDisk,
765 R: RangeBounds<K>,
766 {
767 self.zs_range_iter_with_direction(cf, range, false)
768 }
769
770 /// Returns a reverse iterator over the items in `cf` in `range`.
771 ///
772 /// Holding this iterator open might delay block commit transactions.
773 pub fn zs_reverse_range_iter<C, K, V, R>(
774 &self,
775 cf: &C,
776 range: R,
777 ) -> impl Iterator<Item = (K, V)> + '_
778 where
779 C: rocksdb::AsColumnFamilyRef,
780 K: IntoDisk + FromDisk,
781 V: FromDisk,
782 R: RangeBounds<K>,
783 {
784 self.zs_range_iter_with_direction(cf, range, true)
785 }
786
787 /// Returns an iterator over the items in `cf` in `range`.
788 ///
789 /// RocksDB iterators are ordered by increasing key bytes by default.
790 /// Otherwise, if `reverse` is `true`, the iterator is ordered by decreasing key bytes.
791 ///
792 /// Holding this iterator open might delay block commit transactions.
793 fn zs_range_iter_with_direction<C, K, V, R>(
794 &self,
795 cf: &C,
796 range: R,
797 reverse: bool,
798 ) -> impl Iterator<Item = (K, V)> + '_
799 where
800 C: rocksdb::AsColumnFamilyRef,
801 K: IntoDisk + FromDisk,
802 V: FromDisk,
803 R: RangeBounds<K>,
804 {
805 use std::ops::Bound::{self, *};
806
807 // Replace with map() when it stabilises:
808 // https://github.com/rust-lang/rust/issues/86026
809 let map_to_vec = |bound: Bound<&K>| -> Bound<Vec<u8>> {
810 match bound {
811 Unbounded => Unbounded,
812 Included(x) => Included(x.as_bytes().as_ref().to_vec()),
813 Excluded(x) => Excluded(x.as_bytes().as_ref().to_vec()),
814 }
815 };
816
817 let start_bound = map_to_vec(range.start_bound());
818 let end_bound = map_to_vec(range.end_bound());
819 let range = (start_bound, end_bound);
820
821 let mode = Self::zs_iter_mode(&range, reverse);
822 let opts = Self::zs_iter_opts(&range);
823
824 // Reading multiple items from iterators has caused database hangs,
825 // in previous RocksDB versions
826 self.db
827 .iterator_cf_opt(cf, opts, mode)
828 .map(|result| result.expect("unexpected database failure"))
829 .map(|(key, value)| (key.to_vec(), value))
830 // Skip excluded "from" bound and empty ranges. The `mode` already skips keys
831 // strictly before the "from" bound.
832 .skip_while({
833 let range = range.clone();
834 move |(key, _value)| !range.contains(key)
835 })
836 // Take until the excluded "to" bound is reached,
837 // or we're after the included "to" bound.
838 .take_while(move |(key, _value)| range.contains(key))
839 .map(|(key, value)| (K::from_bytes(key), V::from_bytes(value)))
840 }
841
842 /// Returns the RocksDB ReadOptions with a lower and upper bound for a range.
843 fn zs_iter_opts<R>(range: &R) -> ReadOptions
844 where
845 R: RangeBounds<Vec<u8>>,
846 {
847 let mut opts = ReadOptions::default();
848 let (lower_bound, upper_bound) = Self::zs_iter_bounds(range);
849
850 if let Some(bound) = lower_bound {
851 opts.set_iterate_lower_bound(bound);
852 };
853
854 if let Some(bound) = upper_bound {
855 opts.set_iterate_upper_bound(bound);
856 };
857
858 opts
859 }
860
861 /// Returns a lower and upper iterate bounds for a range.
862 ///
863 /// Note: Since upper iterate bounds are always exclusive in RocksDB, this method
864 /// will increment the upper bound by 1 if the end bound of the provided range
865 /// is inclusive.
866 fn zs_iter_bounds<R>(range: &R) -> (Option<Vec<u8>>, Option<Vec<u8>>)
867 where
868 R: RangeBounds<Vec<u8>>,
869 {
870 use std::ops::Bound::*;
871
872 let lower_bound = match range.start_bound() {
873 Included(bound) | Excluded(bound) => Some(bound.clone()),
874 Unbounded => None,
875 };
876
877 let upper_bound = match range.end_bound().cloned() {
878 Included(mut bound) => {
879 // Increment the last byte in the upper bound that is less than u8::MAX, and
880 // clear any bytes after it to increment the next key in lexicographic order
881 // (next big-endian number). RocksDB uses lexicographic order for keys.
882 let is_wrapped_overflow = increment_big_endian(&mut bound);
883
884 if is_wrapped_overflow {
885 bound.insert(0, 0x01)
886 }
887
888 Some(bound)
889 }
890 Excluded(bound) => Some(bound),
891 Unbounded => None,
892 };
893
894 (lower_bound, upper_bound)
895 }
896
897 /// Returns the RocksDB iterator "from" mode for `range`.
898 ///
899 /// RocksDB iterators are ordered by increasing key bytes by default.
900 /// Otherwise, if `reverse` is `true`, the iterator is ordered by decreasing key bytes.
901 fn zs_iter_mode<R>(range: &R, reverse: bool) -> rocksdb::IteratorMode<'_>
902 where
903 R: RangeBounds<Vec<u8>>,
904 {
905 use std::ops::Bound::*;
906
907 let from_bound = if reverse {
908 range.end_bound()
909 } else {
910 range.start_bound()
911 };
912
913 match from_bound {
914 Unbounded => {
915 if reverse {
916 // Reversed unbounded iterators start from the last item
917 rocksdb::IteratorMode::End
918 } else {
919 // Unbounded iterators start from the first item
920 rocksdb::IteratorMode::Start
921 }
922 }
923
924 Included(bound) | Excluded(bound) => {
925 let direction = if reverse {
926 rocksdb::Direction::Reverse
927 } else {
928 rocksdb::Direction::Forward
929 };
930
931 rocksdb::IteratorMode::From(bound.as_slice(), direction)
932 }
933 }
934 }
935
936 /// The ideal open file limit for Zebra
937 const IDEAL_OPEN_FILE_LIMIT: u64 = 1024;
938
939 /// The minimum number of open files for Zebra to operate normally. Also used
940 /// as the default open file limit, when the OS doesn't tell us how many
941 /// files we can use.
942 ///
943 /// We want 100+ file descriptors for peers, and 100+ for the database.
944 ///
945 /// On Windows, the default limit is 512 high-level I/O files, and 8192
946 /// low-level I/O files:
947 /// <https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/setmaxstdio?view=msvc-160#remarks>
948 const MIN_OPEN_FILE_LIMIT: u64 = 512;
949
950 /// The number of files used internally by Zebra.
951 ///
952 /// Zebra uses file descriptors for OS libraries (10+), polling APIs (10+),
953 /// stdio (3), and other OS facilities (2+).
954 const RESERVED_FILE_COUNT: u64 = 48;
955
956 /// The size of the database memtable RAM cache in megabytes.
957 ///
958 /// <https://github.com/facebook/rocksdb/wiki/RocksDB-FAQ#configuration-and-tuning>
959 const MEMTABLE_RAM_CACHE_MEGABYTES: usize = 128;
960
961 /// Build a vector of current column families on the disk and optionally any new column families.
962 /// Returns an iterable collection of all column families.
963 fn construct_column_families(
964 db_options: Options,
965 path: &Path,
966 column_families_in_code: impl IntoIterator<Item = String>,
967 ) -> impl Iterator<Item = ColumnFamilyDescriptor> {
968 // When opening the database in read/write mode, all column families must be opened.
969 //
970 // To make Zebra forward-compatible with databases updated by later versions,
971 // we read any existing column families off the disk, then add any new column families
972 // from the current implementation.
973 //
974 // <https://github.com/facebook/rocksdb/wiki/Column-Families#reference>
975 let column_families_on_disk = DB::list_cf(&db_options, path).unwrap_or_default();
976 let column_families_in_code = column_families_in_code.into_iter();
977
978 column_families_on_disk
979 .into_iter()
980 .chain(column_families_in_code)
981 .unique()
982 .map(move |cf_name: String| {
983 let mut cf_options = db_options.clone();
984
985 if cf_name == BALANCE_BY_TRANSPARENT_ADDR {
986 cf_options.set_merge_operator_associative(
987 BALANCE_BY_TRANSPARENT_ADDR_MERGE_OP,
988 fetch_add_balance_and_received,
989 );
990 }
991
992 rocksdb::ColumnFamilyDescriptor::new(cf_name, cf_options.clone())
993 })
994 }
995
996 /// Opens or creates the database at a path based on the kind, major version and network,
997 /// with the supplied column families, preserving any existing column families,
998 /// and returns a shared low-level database wrapper.
999 ///
1000 /// # Errors
1001 ///
1002 /// - In read-only mode, if the cache directory is missing or unreadable.
1003 ///
1004 /// # Panics
1005 ///
1006 /// - In read-write mode, if the cache directory does not exist and can't be created.
1007 /// - If the database cannot be opened for whatever reason.
1008 pub fn new(
1009 config: &Config,
1010 db_kind: impl AsRef<str>,
1011 format_version_in_code: &Version,
1012 network: &Network,
1013 column_families_in_code: impl IntoIterator<Item = String>,
1014 read_only: bool,
1015 ) -> Result<DiskDb, StateInitError> {
1016 // `ephemeral` and `read_only` describe mutually-exclusive modes: an ephemeral
1017 // database is a read-write primary this process owns and deletes on drop, while
1018 // a read-only secondary follows another process's primary and must never delete
1019 // it. Combining them is a configuration error — it would delete the primary's
1020 // files on drop — so reject it here rather than silently coercing it (a check
1021 // that must hold in release builds, not just debug).
1022 let mode = match (read_only, config.ephemeral) {
1023 (true, true) => return Err(StateInitError::ReadOnlyEphemeralConflict),
1024 (true, false) => DbMode::ReadOnlySecondary,
1025 (false, true) => DbMode::Ephemeral,
1026 (false, false) => DbMode::Persistent,
1027 };
1028
1029 // If the database is ephemeral, we don't need to check the cache directory.
1030 if !config.ephemeral {
1031 if read_only {
1032 // A read-only secondary instance must never create the primary cache
1033 // directory: the primary zebrad owns it. At most, verify it already
1034 // exists and is readable, and fail with a clear error otherwise.
1035 DiskDb::check_cache_dir_readable(&config.cache_dir)?;
1036 } else {
1037 DiskDb::validate_cache_dir(&config.cache_dir);
1038 }
1039 }
1040
1041 let db_kind = db_kind.as_ref();
1042 let path = config.db_path(db_kind, format_version_in_code.major, network);
1043
1044 let db_options = DiskDb::options();
1045
1046 let column_families =
1047 DiskDb::construct_column_families(db_options.clone(), &path, column_families_in_code);
1048
1049 let db_result = if read_only {
1050 // Use a tempfile for the secondary instance cache directory
1051 let secondary_config = Config {
1052 ephemeral: true,
1053 ..config.clone()
1054 };
1055 let secondary_path =
1056 secondary_config.db_path("secondary_state", format_version_in_code.major, network);
1057 let create_dir_result = std::fs::create_dir_all(&secondary_path);
1058
1059 info!(?create_dir_result, "creating secondary db directory");
1060
1061 DB::open_cf_descriptors_as_secondary(
1062 &db_options,
1063 &path,
1064 &secondary_path,
1065 column_families,
1066 )
1067 } else {
1068 DB::open_cf_descriptors(&db_options, &path, column_families)
1069 };
1070
1071 match db_result {
1072 Ok(db) => {
1073 info!("Opened Zebra state cache at {}", path.display());
1074
1075 let db = DiskDb {
1076 db_kind: db_kind.to_string(),
1077 format_version_in_code: format_version_in_code.clone(),
1078 network: network.clone(),
1079 mode,
1080 db: Arc::new(db),
1081 finished_format_upgrades: Arc::new(AtomicBool::new(false)),
1082 };
1083
1084 db.assert_default_cf_is_empty();
1085
1086 Ok(db)
1087 }
1088
1089 Err(e) if matches!(e.kind(), ErrorKind::Busy | ErrorKind::IOError) => panic!(
1090 "Database likely already open {path:?} \
1091 Hint: Check if another zebrad process is running."
1092 ),
1093
1094 Err(e) => panic!(
1095 "Opening database {path:?} failed. \
1096 Hint: Try changing the state cache_dir in the Zebra config. \
1097 Error: {e}",
1098 ),
1099 }
1100 }
1101
1102 // Accessor methods
1103
1104 /// Returns the configured database kind for this database.
1105 pub fn db_kind(&self) -> String {
1106 self.db_kind.clone()
1107 }
1108
1109 /// Returns the format version of the running code that created this `DiskDb` instance in memory.
1110 pub fn format_version_in_code(&self) -> Version {
1111 self.format_version_in_code.clone()
1112 }
1113
1114 /// Returns the fixed major version for this database.
1115 pub fn major_version(&self) -> u64 {
1116 self.format_version_in_code().major
1117 }
1118
1119 /// Returns the configured network for this database.
1120 pub fn network(&self) -> Network {
1121 self.network.clone()
1122 }
1123
1124 /// Returns the `Path` where the files used by this database are located.
1125 pub fn path(&self) -> &Path {
1126 self.db.path()
1127 }
1128
1129 /// Returns the low-level rocksdb inner database.
1130 #[allow(dead_code)]
1131 fn inner(&self) -> &Arc<DB> {
1132 &self.db
1133 }
1134
1135 /// Returns the column family handle for `cf_name`.
1136 pub fn cf_handle(&self, cf_name: &str) -> Option<rocksdb::ColumnFamilyRef<'_>> {
1137 // Note: the lifetime returned by this method is subtly wrong. As of December 2023 it is
1138 // the shorter of &self and &str, but RocksDB clones column family names internally, so it
1139 // should just be &self. To avoid this restriction, clone the string before passing it to
1140 // this method. Currently Zebra uses static strings, so this doesn't matter.
1141 self.db.cf_handle(cf_name)
1142 }
1143
1144 // Read methods are located in the ReadDisk trait
1145
1146 // Write methods
1147 // Low-level write methods are located in the WriteDisk trait
1148
1149 /// Writes `batch` to the database.
1150 pub(crate) fn write(&self, batch: DiskWriteBatch) -> Result<(), rocksdb::Error> {
1151 self.db.write(batch.batch)
1152 }
1153
1154 // Private methods
1155
1156 /// Tries to reuse an existing db after a major upgrade.
1157 ///
1158 /// If the current db version belongs to `restorable_db_versions`, the function moves a previous
1159 /// db to a new path so it can be used again. It does so by merely trying to rename the path
1160 /// corresponding to the db version directly preceding the current version to the path that is
1161 /// used by the current db. If successful, it also deletes the db version file.
1162 ///
1163 /// Returns the old disk version if one existed and the db directory was renamed, or None otherwise.
1164 // TODO: Update this function to rename older major db format version to the current version (#9565).
1165 #[allow(clippy::unwrap_in_result)]
1166 pub(crate) fn try_reusing_previous_db_after_major_upgrade(
1167 restorable_db_versions: &[u64],
1168 format_version_in_code: &Version,
1169 config: &Config,
1170 db_kind: impl AsRef<str>,
1171 network: &Network,
1172 ) -> Option<Version> {
1173 if let Some(&major_db_ver) = restorable_db_versions
1174 .iter()
1175 .find(|v| **v == format_version_in_code.major)
1176 {
1177 let db_kind = db_kind.as_ref();
1178
1179 let old_major_db_ver = major_db_ver - 1;
1180 let old_path = config.db_path(db_kind, old_major_db_ver, network);
1181 // Exit early if the path doesn't exist or there's an error checking it.
1182 if !fs::exists(&old_path).unwrap_or(false) {
1183 return None;
1184 }
1185
1186 let new_path = config.db_path(db_kind, major_db_ver, network);
1187
1188 let old_path = match fs::canonicalize(&old_path) {
1189 Ok(canonicalized_old_path) => canonicalized_old_path,
1190 Err(e) => {
1191 warn!("could not canonicalize {old_path:?}: {e}");
1192 return None;
1193 }
1194 };
1195
1196 let cache_path = match fs::canonicalize(&config.cache_dir) {
1197 Ok(canonicalized_cache_path) => canonicalized_cache_path,
1198 Err(e) => {
1199 warn!("could not canonicalize {:?}: {e}", config.cache_dir);
1200 return None;
1201 }
1202 };
1203
1204 // # Correctness
1205 //
1206 // Check that the path we're about to move is inside the cache directory.
1207 //
1208 // If the user has symlinked the state directory to a non-cache directory, we don't want
1209 // to move it, because it might contain other files.
1210 //
1211 // We don't attempt to guard against malicious symlinks created by attackers
1212 // (TOCTOU attacks). Zebra should not be run with elevated privileges.
1213 if !old_path.starts_with(&cache_path) {
1214 info!("skipped reusing previous state cache: state is outside cache directory");
1215 return None;
1216 }
1217
1218 let opts = DiskDb::options();
1219 let old_db_exists = DB::list_cf(&opts, &old_path).is_ok_and(|cf| !cf.is_empty());
1220 let new_db_exists = DB::list_cf(&opts, &new_path).is_ok_and(|cf| !cf.is_empty());
1221
1222 if old_db_exists && !new_db_exists {
1223 // Create the parent directory for the new db. This is because we can't directly
1224 // rename e.g. `state/v25/mainnet/` to `state/v26/mainnet/` with `fs::rename()` if
1225 // `state/v26/` does not exist.
1226 match fs::create_dir_all(
1227 new_path
1228 .parent()
1229 .expect("new state cache must have a parent path"),
1230 ) {
1231 Ok(()) => info!("created new directory for state cache at {new_path:?}"),
1232 Err(e) => {
1233 warn!(
1234 "could not create new directory for state cache at {new_path:?}: {e}"
1235 );
1236 return None;
1237 }
1238 };
1239
1240 match fs::rename(&old_path, &new_path) {
1241 Ok(()) => {
1242 info!("moved state cache from {old_path:?} to {new_path:?}");
1243
1244 let mut disk_version =
1245 database_format_version_on_disk(config, db_kind, major_db_ver, network)
1246 .expect("unable to read database format version file")
1247 .expect("unable to parse database format version");
1248
1249 disk_version.major = old_major_db_ver;
1250
1251 write_database_format_version_to_disk(
1252 config,
1253 db_kind,
1254 major_db_ver,
1255 &disk_version,
1256 network,
1257 )
1258 .expect("unable to write database format version file to disk");
1259
1260 // Get the parent of the old path, e.g. `state/v25/` and delete it if it is
1261 // empty.
1262 let old_path = old_path
1263 .parent()
1264 .expect("old state cache must have parent path");
1265
1266 if fs::read_dir(old_path)
1267 .expect("cached state dir needs to be readable")
1268 .next()
1269 .is_none()
1270 {
1271 match fs::remove_dir_all(old_path) {
1272 Ok(()) => {
1273 info!("removed empty old state cache directory at {old_path:?}")
1274 }
1275 Err(e) => {
1276 warn!(
1277 "could not remove empty old state cache directory \
1278 at {old_path:?}: {e}"
1279 )
1280 }
1281 }
1282 }
1283
1284 return Some(disk_version);
1285 }
1286 Err(e) => {
1287 warn!("could not move state cache from {old_path:?} to {new_path:?}: {e}");
1288 }
1289 };
1290 }
1291 };
1292
1293 None
1294 }
1295
1296 /// Returns the database options for the finalized state database.
1297 fn options() -> rocksdb::Options {
1298 let mut opts = rocksdb::Options::default();
1299 let mut block_based_opts = rocksdb::BlockBasedOptions::default();
1300
1301 const ONE_MEGABYTE: usize = 1024 * 1024;
1302
1303 opts.create_if_missing(true);
1304 opts.create_missing_column_families(true);
1305
1306 // Use the recommended Ribbon filter setting for all column families.
1307 //
1308 // Ribbon filters are faster than Bloom filters in Zebra, as of April 2022.
1309 // (They aren't needed for single-valued column families, but they don't hurt either.)
1310 block_based_opts.set_ribbon_filter(9.9);
1311
1312 // Use the recommended LZ4 compression type.
1313 //
1314 // https://github.com/facebook/rocksdb/wiki/Compression#configuration
1315 opts.set_compression_type(rocksdb::DBCompressionType::Lz4);
1316
1317 // Tune level-style database file compaction.
1318 //
1319 // This improves Zebra's initial sync speed slightly, as of April 2022.
1320 opts.optimize_level_style_compaction(Self::MEMTABLE_RAM_CACHE_MEGABYTES * ONE_MEGABYTE);
1321
1322 // Increase the process open file limit if needed,
1323 // then use it to set RocksDB's limit.
1324 let open_file_limit = DiskDb::increase_open_file_limit();
1325 let db_file_limit = DiskDb::get_db_open_file_limit(open_file_limit);
1326
1327 // If the current limit is very large, set the DB limit using the ideal limit
1328 let ideal_limit = DiskDb::get_db_open_file_limit(DiskDb::IDEAL_OPEN_FILE_LIMIT)
1329 .try_into()
1330 .expect("ideal open file limit fits in a c_int");
1331 let db_file_limit = db_file_limit.try_into().unwrap_or(ideal_limit);
1332
1333 opts.set_max_open_files(db_file_limit);
1334
1335 // Set the block-based options
1336 opts.set_block_based_table_factory(&block_based_opts);
1337
1338 opts
1339 }
1340
1341 /// Calculate the database's share of `open_file_limit`
1342 fn get_db_open_file_limit(open_file_limit: u64) -> u64 {
1343 // Give the DB half the files, and reserve half the files for peers
1344 (open_file_limit - DiskDb::RESERVED_FILE_COUNT) / 2
1345 }
1346
1347 /// Increase the open file limit for this process to `IDEAL_OPEN_FILE_LIMIT`.
1348 /// If that fails, try `MIN_OPEN_FILE_LIMIT`.
1349 ///
1350 /// If the current limit is above `IDEAL_OPEN_FILE_LIMIT`, leaves it
1351 /// unchanged.
1352 ///
1353 /// Returns the current limit, after any successful increases.
1354 ///
1355 /// # Panics
1356 ///
1357 /// If the open file limit can not be increased to `MIN_OPEN_FILE_LIMIT`.
1358 fn increase_open_file_limit() -> u64 {
1359 // Zebra mainly uses TCP sockets (`zebra-network`) and low-level files
1360 // (`zebra-state` database).
1361 //
1362 // On Unix-based platforms, `increase_nofile_limit` changes the limit for
1363 // both database files and TCP connections.
1364 //
1365 // But it doesn't do anything on Windows in rlimit 0.7.0.
1366 //
1367 // On Windows, the default limits are:
1368 // - 512 high-level stream I/O files (via the C standard functions),
1369 // - 8192 low-level I/O files (via the Unix C functions), and
1370 // - 1000 TCP Control Block entries (network connections).
1371 //
1372 // https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/setmaxstdio?view=msvc-160#remarks
1373 // http://smallvoid.com/article/winnt-tcpip-max-limit.html
1374 //
1375 // `zebra-state`'s `IDEAL_OPEN_FILE_LIMIT` is much less than
1376 // the Windows low-level I/O file limit.
1377 //
1378 // The [`setmaxstdio` and `getmaxstdio`](https://docs.rs/rlimit/latest/rlimit/#windows)
1379 // functions from the `rlimit` crate only change the high-level I/O file limit.
1380 //
1381 // `zebra-network`'s default connection limit is much less than
1382 // the TCP Control Block limit on Windows.
1383
1384 // We try setting the ideal limit, then the minimum limit.
1385 let current_limit = match increase_nofile_limit(DiskDb::IDEAL_OPEN_FILE_LIMIT) {
1386 Ok(current_limit) => current_limit,
1387 Err(limit_error) => {
1388 // These errors can happen due to sandboxing or unsupported system calls,
1389 // even if the file limit is high enough.
1390 info!(
1391 ?limit_error,
1392 min_limit = ?DiskDb::MIN_OPEN_FILE_LIMIT,
1393 ideal_limit = ?DiskDb::IDEAL_OPEN_FILE_LIMIT,
1394 "unable to increase the open file limit, \
1395 assuming Zebra can open a minimum number of files"
1396 );
1397
1398 return DiskDb::MIN_OPEN_FILE_LIMIT;
1399 }
1400 };
1401
1402 if current_limit < DiskDb::MIN_OPEN_FILE_LIMIT {
1403 panic!(
1404 "open file limit too low: \
1405 unable to set the number of open files to {}, \
1406 the minimum number of files required by Zebra. \
1407 Current limit is {:?}. \
1408 Hint: Increase the open file limit to {} before launching Zebra",
1409 DiskDb::MIN_OPEN_FILE_LIMIT,
1410 current_limit,
1411 DiskDb::IDEAL_OPEN_FILE_LIMIT
1412 );
1413 } else if current_limit < DiskDb::IDEAL_OPEN_FILE_LIMIT {
1414 warn!(
1415 ?current_limit,
1416 min_limit = ?DiskDb::MIN_OPEN_FILE_LIMIT,
1417 ideal_limit = ?DiskDb::IDEAL_OPEN_FILE_LIMIT,
1418 "the maximum number of open files is below Zebra's ideal limit. \
1419 Hint: Increase the open file limit to {} before launching Zebra",
1420 DiskDb::IDEAL_OPEN_FILE_LIMIT
1421 );
1422 } else if cfg!(windows) {
1423 // This log is verbose during tests.
1424 #[cfg(not(test))]
1425 info!(
1426 min_limit = ?DiskDb::MIN_OPEN_FILE_LIMIT,
1427 ideal_limit = ?DiskDb::IDEAL_OPEN_FILE_LIMIT,
1428 "assuming the open file limit is high enough for Zebra",
1429 );
1430 #[cfg(test)]
1431 debug!(
1432 min_limit = ?DiskDb::MIN_OPEN_FILE_LIMIT,
1433 ideal_limit = ?DiskDb::IDEAL_OPEN_FILE_LIMIT,
1434 "assuming the open file limit is high enough for Zebra",
1435 );
1436 } else {
1437 #[cfg(not(test))]
1438 debug!(
1439 ?current_limit,
1440 min_limit = ?DiskDb::MIN_OPEN_FILE_LIMIT,
1441 ideal_limit = ?DiskDb::IDEAL_OPEN_FILE_LIMIT,
1442 "the open file limit is high enough for Zebra",
1443 );
1444 #[cfg(test)]
1445 debug!(
1446 ?current_limit,
1447 min_limit = ?DiskDb::MIN_OPEN_FILE_LIMIT,
1448 ideal_limit = ?DiskDb::IDEAL_OPEN_FILE_LIMIT,
1449 "the open file limit is high enough for Zebra",
1450 );
1451 }
1452
1453 current_limit
1454 }
1455
1456 // Cleanup methods
1457
1458 /// Returns the number of shared instances of this database.
1459 ///
1460 /// # Concurrency
1461 ///
1462 /// The actual number of owners can be higher or lower than the returned value,
1463 /// because databases can simultaneously be cloned or dropped in other threads.
1464 ///
1465 /// However, if the number of owners is 1, and the caller has exclusive access,
1466 /// the count can't increase unless that caller clones the database.
1467 pub(crate) fn shared_database_owners(&self) -> usize {
1468 Arc::strong_count(&self.db) + Arc::weak_count(&self.db)
1469 }
1470
1471 /// Shut down the database, cleaning up background tasks and ephemeral data.
1472 ///
1473 /// If `force` is true, clean up regardless of any shared references.
1474 /// `force` can cause errors accessing the database from other shared references.
1475 /// It should only be used in debugging or test code, immediately before a manual shutdown.
1476 ///
1477 /// TODO: make private after the stop height check has moved to the syncer (#3442)
1478 /// move shutting down the database to a blocking thread (#2188)
1479 pub(crate) fn shutdown(&mut self, force: bool) {
1480 // # Correctness
1481 //
1482 // If we're the only owner of the shared database instance,
1483 // then there are no other threads that can increase the strong or weak count.
1484 //
1485 // ## Implementation Requirements
1486 //
1487 // This function and all functions that it calls should avoid cloning the shared database
1488 // instance. If they do, they must drop it before:
1489 // - shutting down database threads, or
1490 // - deleting database files.
1491
1492 if self.shared_database_owners() > 1 {
1493 let path = self.path();
1494
1495 let mut ephemeral_note = "";
1496
1497 if force {
1498 if self.mode.deletes_files_on_drop() {
1499 ephemeral_note = " and removing ephemeral files";
1500 }
1501
1502 // This log is verbose during tests.
1503 #[cfg(not(test))]
1504 info!(
1505 ?path,
1506 "forcing shutdown{} of a state database with multiple active instances",
1507 ephemeral_note,
1508 );
1509 #[cfg(test)]
1510 debug!(
1511 ?path,
1512 "forcing shutdown{} of a state database with multiple active instances",
1513 ephemeral_note,
1514 );
1515 } else {
1516 if self.mode.deletes_files_on_drop() {
1517 ephemeral_note = " and files";
1518 }
1519
1520 debug!(
1521 ?path,
1522 "dropping DiskDb clone, \
1523 but keeping shared database instance{} until the last reference is dropped",
1524 ephemeral_note,
1525 );
1526 return;
1527 }
1528 }
1529
1530 self.assert_default_cf_is_empty();
1531
1532 // Drop isn't guaranteed to run, such as when we panic, or if the tokio shutdown times out.
1533 //
1534 // Zebra's data should be fine if we don't clean up, because:
1535 // - the database flushes regularly anyway
1536 // - Zebra commits each block in a database transaction, any incomplete blocks get rolled back
1537 // - ephemeral files are placed in the os temp dir and should be cleaned up automatically eventually
1538 let path = self.path();
1539 debug!(?path, "flushing database to disk");
1540
1541 // A read-only secondary instance has nothing to flush, and RocksDB rejects
1542 // `flush()`/`flush_wal()` on secondaries with "Not supported operation in
1543 // secondary mode". Only a read-write primary needs flushing on shutdown.
1544 if !self.mode.is_read_only() {
1545 // These flushes can fail during forced shutdown or during Drop after a shutdown,
1546 // particularly in tests. If they fail, there's nothing we can do about it anyway.
1547 if let Err(error) = self.db.flush() {
1548 if matches!(error.kind(), ErrorKind::ShutdownInProgress) {
1549 debug!(
1550 ?error,
1551 ?path,
1552 "expected shutdown error flushing database SST files to disk"
1553 );
1554 } else {
1555 info!(
1556 ?error,
1557 ?path,
1558 "unexpected error flushing database SST files to disk during shutdown"
1559 );
1560 }
1561 }
1562
1563 if let Err(error) = self.db.flush_wal(true) {
1564 if matches!(error.kind(), ErrorKind::ShutdownInProgress) {
1565 debug!(
1566 ?error,
1567 ?path,
1568 "expected shutdown error flushing database WAL buffer to disk"
1569 );
1570 } else {
1571 info!(
1572 ?error,
1573 ?path,
1574 "unexpected error flushing database WAL buffer to disk during shutdown"
1575 );
1576 }
1577 }
1578 }
1579
1580 // # Memory Safety
1581 //
1582 // We'd like to call `cancel_all_background_work()` before Zebra exits,
1583 // but when we call it, we get memory, thread, or C++ errors when the process exits.
1584 // (This seems to be a bug in RocksDB: cancel_all_background_work() should wait until
1585 // all the threads have cleaned up.)
1586 //
1587 // # Change History
1588 //
1589 // We've changed this setting multiple times since 2021, in response to new RocksDB
1590 // and Rust compiler behaviour.
1591 //
1592 // We enabled cancel_all_background_work() due to failures on:
1593 // - Rust 1.57 on Linux
1594 //
1595 // We disabled cancel_all_background_work() due to failures on:
1596 // - Rust 1.64 on Linux
1597 //
1598 // We tried enabling cancel_all_background_work() due to failures on:
1599 // - Rust 1.70 on macOS 12.6.5 on x86_64
1600 // but it didn't stop the aborts happening (PR #6820).
1601 //
1602 // There weren't any failures with cancel_all_background_work() disabled on:
1603 // - Rust 1.69 or earlier
1604 // - Linux with Rust 1.70
1605 // And with cancel_all_background_work() enabled or disabled on:
1606 // - macOS 13.2 on aarch64 (M1), native and emulated x86_64, with Rust 1.70
1607 //
1608 // # Detailed Description
1609 //
1610 // We see these kinds of errors:
1611 // ```
1612 // pthread lock: Invalid argument
1613 // pure virtual method called
1614 // terminate called without an active exception
1615 // pthread destroy mutex: Device or resource busy
1616 // Aborted (core dumped)
1617 // signal: 6, SIGABRT: process abort signal
1618 // signal: 11, SIGSEGV: invalid memory reference
1619 // ```
1620 //
1621 // # Reference
1622 //
1623 // The RocksDB wiki says:
1624 // > Q: Is it safe to close RocksDB while another thread is issuing read, write or manual compaction requests?
1625 // >
1626 // > A: No. The users of RocksDB need to make sure all functions have finished before they close RocksDB.
1627 // > You can speed up the waiting by calling CancelAllBackgroundWork().
1628 //
1629 // <https://github.com/facebook/rocksdb/wiki/RocksDB-FAQ>
1630 //
1631 // > rocksdb::DB instances need to be destroyed before your main function exits.
1632 // > RocksDB instances usually depend on some internal static variables.
1633 // > Users need to make sure rocksdb::DB instances are destroyed before those static variables.
1634 //
1635 // <https://github.com/facebook/rocksdb/wiki/Known-Issues>
1636 //
1637 // # TODO
1638 //
1639 // Try re-enabling this code and fixing the underlying concurrency bug.
1640 //
1641 //info!(?path, "stopping background database tasks");
1642 //self.db.cancel_all_background_work(true);
1643
1644 // We'd like to drop the database before deleting its files,
1645 // because that closes the column families and the database correctly.
1646 // But Rust's ownership rules make that difficult,
1647 // so we just flush and delete ephemeral data instead.
1648 //
1649 // This implementation doesn't seem to cause any issues,
1650 // and the RocksDB Drop implementation handles any cleanup.
1651 self.delete_ephemeral();
1652 }
1653
1654 /// If the database is `ephemeral`, delete its files.
1655 fn delete_ephemeral(&mut self) {
1656 // # Correctness
1657 //
1658 // This function and all functions that it calls should avoid cloning the shared database
1659 // instance. See `shutdown()` for details.
1660
1661 if !self.mode.deletes_files_on_drop() {
1662 return;
1663 }
1664
1665 let path = self.path();
1666
1667 // This log is verbose during tests.
1668 #[cfg(not(test))]
1669 info!(?path, "removing temporary database files");
1670 #[cfg(test)]
1671 debug!(?path, "removing temporary database files");
1672
1673 // We'd like to use `rocksdb::Env::mem_env` for ephemeral databases,
1674 // but the Zcash blockchain might not fit in memory. So we just
1675 // delete the database files instead.
1676 //
1677 // We'd also like to call `DB::destroy` here, but calling destroy on a
1678 // live DB is undefined behaviour:
1679 // https://github.com/facebook/rocksdb/wiki/RocksDB-FAQ#basic-readwrite
1680 //
1681 // So we assume that all the database files are under `path`, and
1682 // delete them using standard filesystem APIs. Deleting open files
1683 // might cause errors on non-Unix platforms, so we ignore the result.
1684 // (The OS will delete them eventually anyway, if they are in a temporary directory.)
1685 let result = std::fs::remove_dir_all(path);
1686
1687 if result.is_err() {
1688 // This log is verbose during tests.
1689 #[cfg(not(test))]
1690 info!(
1691 ?result,
1692 ?path,
1693 "removing temporary database files caused an error",
1694 );
1695 #[cfg(test)]
1696 debug!(
1697 ?result,
1698 ?path,
1699 "removing temporary database files caused an error",
1700 );
1701 } else {
1702 debug!(
1703 ?result,
1704 ?path,
1705 "successfully removed temporary database files",
1706 );
1707 }
1708 }
1709
1710 /// Check that the "default" column family is empty.
1711 ///
1712 /// # Panics
1713 ///
1714 /// If Zebra has a bug where it is storing data in the wrong column family.
1715 fn assert_default_cf_is_empty(&self) {
1716 // # Correctness
1717 //
1718 // This function and all functions that it calls should avoid cloning the shared database
1719 // instance. See `shutdown()` for details.
1720
1721 if let Some(default_cf) = self.cf_handle("default") {
1722 assert!(
1723 self.zs_is_empty(&default_cf),
1724 "Zebra should not store data in the 'default' column family"
1725 );
1726 }
1727 }
1728
1729 // Checks that a cache directory already exists and is readable, without creating it.
1730 //
1731 // Used when opening a read-only secondary instance, which must never create the
1732 // primary's cache directory. Returns a [`StateInitError`] if the directory is missing
1733 // or unreadable.
1734 pub(crate) fn check_cache_dir_readable(
1735 cache_dir: &std::path::Path,
1736 ) -> Result<(), StateInitError> {
1737 match fs::read_dir(cache_dir) {
1738 Ok(_) => Ok(()),
1739 Err(source) => Err(StateInitError::ReadOnlyCacheDirUnreadable {
1740 path: cache_dir.to_path_buf(),
1741 source,
1742 }),
1743 }
1744 }
1745
1746 // Validates a cache directory and creates it if it doesn't exist.
1747 // If the directory cannot be created, it panics with a specific error message.
1748 fn validate_cache_dir(cache_dir: &std::path::PathBuf) {
1749 if let Err(e) = fs::create_dir_all(cache_dir) {
1750 match e.kind() {
1751 std::io::ErrorKind::PermissionDenied => panic!(
1752 "Permission denied creating {cache_dir:?}. \
1753 Hint: check if cache directory exist and has write permissions."
1754 ),
1755 std::io::ErrorKind::StorageFull => panic!(
1756 "No space left on device creating {cache_dir:?}. \
1757 Hint: check if the disk is full."
1758 ),
1759 _ => panic!("Could not create cache dir {cache_dir:?}: {e}"),
1760 }
1761 }
1762 }
1763}
1764
1765impl Drop for DiskDb {
1766 fn drop(&mut self) {
1767 let path = self.path();
1768 debug!(?path, "dropping DiskDb instance");
1769
1770 self.shutdown(false);
1771 }
1772}