1pub mod errors;
6pub(crate) mod options;
7pub(crate) mod rocks_util;
8pub(crate) mod safe_iter;
9mod sst_file;
10
11use std::{
12 collections::HashSet,
13 ffi::CStr,
14 path::{Path, PathBuf},
15 sync::Arc,
16 time::Duration,
17};
18
19use backoff::backoff::Backoff;
20use iota_macros::nondeterministic;
21use rocksdb::{
22 AsColumnFamilyRef, ColumnFamilyDescriptor, Error, MultiThreaded, properties,
23 properties::num_files_at_level,
24};
25use tracing::warn;
26use typed_store_error::TypedStoreError;
27
28pub use crate::{
29 database::{DBBatch, DBMap, MetricConf, TaggedDBMap},
30 rocks::{
31 options::{
32 BulkIngestionOptions, DBMapTableConfigMap, DBOptions, ReadWriteOptions,
33 bulk_ingestion_options, default_db_options, disable_fallocate, list_tables,
34 read_size_from_env, synced_write_options, target_file_size_base,
35 },
36 sst_file::SstFileBuffer,
37 },
38};
39use crate::{
40 database::{Database, Storage},
41 metrics::DBMetrics,
42 rocks::{errors::typed_store_err_from_rocks_err, options::base_db_options},
43};
44
45const ROCKSDB_PROPERTY_TOTAL_BLOB_FILES_SIZE: &CStr =
48 unsafe { CStr::from_bytes_with_nul_unchecked("rocksdb.total-blob-file-size\0".as_bytes()) };
49
50const METRICS_ERROR: i64 = -1;
51
52const DB_CORRUPTED_KEY: &[u8] = b"db_corrupted";
53
54#[cfg(test)]
55mod tests;
56
57#[derive(Debug)]
58pub(crate) struct RocksDB {
59 pub(crate) underlying: rocksdb::DBWithThreadMode<MultiThreaded>,
60}
61
62impl RocksDB {
63 pub(crate) fn cf_names(&self) -> Result<Vec<String>, rocksdb::Error> {
65 rocksdb::DBWithThreadMode::<MultiThreaded>::list_cf(
66 &rocksdb::Options::default(),
67 self.underlying.path(),
68 )
69 }
70}
71
72impl Drop for RocksDB {
73 fn drop(&mut self) {
74 self.underlying.cancel_all_background_work(true);
75 }
76}
77
78pub(crate) fn rocks_cf<'a>(
82 rocks_db: &'a RocksDB,
83 cf_name: &str,
84) -> Result<Arc<rocksdb::BoundColumnFamily<'a>>, TypedStoreError> {
85 rocks_db
86 .underlying
87 .cf_handle(cf_name)
88 .ok_or_else(|| TypedStoreError::UnregisteredColumn(cf_name.to_string()))
89}
90
91pub fn available_disk_space(path: &Path) -> Option<u64> {
95 available_space_of(path.ancestors().find(|ancestor| ancestor.exists())?)
96}
97
98#[cfg(unix)]
99fn available_space_of(path: &Path) -> Option<u64> {
100 let stats = rustix::fs::statvfs(path).ok()?;
101 Some(stats.f_bavail.saturating_mul(stats.f_frsize))
102}
103
104#[cfg(not(unix))]
105fn available_space_of(path: &Path) -> Option<u64> {
106 let path = path.canonicalize().ok()?;
107 sysinfo::Disks::new_with_refreshed_list()
108 .list()
109 .iter()
110 .filter(|disk| path.starts_with(disk.mount_point()))
111 .max_by_key(|disk| disk.mount_point().as_os_str().len())
112 .map(|disk| disk.available_space())
113}
114
115pub fn check_and_mark_db_corruption(path: &Path) -> Result<(), String> {
118 let db = nondeterministic!(open_status_db(path)).map_err(|e| e.to_string())?;
124
125 db.get(DB_CORRUPTED_KEY)
126 .map_err(|e| format!("Failed to open database: {e}"))
127 .and_then(|value| match value {
128 Some(v) if v[0] == 1 => Err(
129 "Database is corrupted, please remove the current database and start clean!"
130 .to_string(),
131 ),
132 Some(_) => Ok(()),
133 None => db
134 .put(DB_CORRUPTED_KEY, [1])
135 .map_err(|e| format!("Failed to set corrupted key in database: {e}")),
136 })?;
137
138 Ok(())
139}
140
141pub fn unmark_db_corruption(path: &Path) -> Result<(), Error> {
142 nondeterministic!(open_status_db(path))?.put(DB_CORRUPTED_KEY, [0])
144}
145
146fn open_status_db(path: &Path) -> Result<rocksdb::DB, Error> {
147 let mut options = base_db_options();
148 options.create_if_missing(true);
149 rocksdb::DB::open(&options, path)
150}
151
152#[tracing::instrument(level = "debug", skip_all, fields(path = ?path.as_ref()), err(level = "warn"))]
157pub fn open_cf_opts<P: AsRef<Path>>(
158 path: P,
159 db_options: Option<rocksdb::Options>,
160 metric_conf: MetricConf,
161 opt_cfs: &[(&str, rocksdb::Options)],
162) -> Result<Arc<Database>, TypedStoreError> {
163 let path = path.as_ref();
164 let cfs = populate_missing_cfs(opt_cfs, path).map_err(typed_store_err_from_rocks_err)?;
174 nondeterministic!({
175 let mut options = db_options.unwrap_or_else(|| default_db_options().options);
176 options.create_if_missing(true);
177 options.create_missing_column_families(true);
178 let rocksdb = {
179 rocksdb::DBWithThreadMode::<MultiThreaded>::open_cf_descriptors(
180 &options,
181 path,
182 cfs.into_iter()
183 .map(|(name, opts)| ColumnFamilyDescriptor::new(name, opts)),
184 )
185 .map_err(typed_store_err_from_rocks_err)?
186 };
187 Ok(Arc::new(Database::new(
188 Storage::Rocks(RocksDB {
189 underlying: rocksdb,
190 }),
191 metric_conf,
192 )))
193 })
194}
195
196pub fn open_cf_opts_secondary<P: AsRef<Path>>(
199 primary_path: P,
200 secondary_path: Option<P>,
201 db_options: Option<rocksdb::Options>,
202 metric_conf: MetricConf,
203 opt_cfs: &[(&str, rocksdb::Options)],
204) -> Result<Arc<Database>, TypedStoreError> {
205 let primary_path = primary_path.as_ref();
206 let secondary_path = secondary_path.as_ref().map(|p| p.as_ref());
207 nondeterministic!({
209 let mut options = db_options.unwrap_or_else(|| default_db_options().options);
211
212 fdlimit::raise_fd_limit();
213 options.set_max_open_files(-1);
215
216 let mut opt_cfs: std::collections::HashMap<_, _> = opt_cfs.iter().cloned().collect();
217 let cfs = rocksdb::DBWithThreadMode::<MultiThreaded>::list_cf(&options, primary_path)
218 .ok()
219 .unwrap_or_default();
220
221 let default_db_options = default_db_options();
222 for cf_key in cfs.iter() {
224 if !opt_cfs.contains_key(&cf_key[..]) {
225 opt_cfs.insert(cf_key, default_db_options.options.clone());
226 }
227 }
228
229 let primary_path = primary_path.to_path_buf();
230 let secondary_path = secondary_path.map(|q| q.to_path_buf()).unwrap_or_else(|| {
231 let mut s = primary_path.clone();
232 s.pop();
233 s.push("SECONDARY");
234 s.as_path().to_path_buf()
235 });
236
237 let rocksdb = {
238 options.create_if_missing(true);
239 options.create_missing_column_families(true);
240 let db = rocksdb::DBWithThreadMode::<MultiThreaded>::open_cf_descriptors_as_secondary(
241 &options,
242 &primary_path,
243 &secondary_path,
244 opt_cfs
245 .iter()
246 .map(|(name, opts)| ColumnFamilyDescriptor::new(*name, (*opts).clone())),
247 )
248 .map_err(typed_store_err_from_rocks_err)?;
249 db.try_catch_up_with_primary()
250 .map_err(typed_store_err_from_rocks_err)?;
251 db
252 };
253 Ok(Arc::new(Database::new(
254 Storage::Rocks(RocksDB {
255 underlying: rocksdb,
256 }),
257 metric_conf,
258 )))
259 })
260}
261
262#[cfg(msim)]
264pub async fn safe_drop_db(path: PathBuf, timeout: Duration) -> Result<(), rocksdb::Error> {
265 nondeterministic!({
271 let deadline = std::time::Instant::now() + timeout;
272 loop {
273 match rocksdb::DB::destroy(&rocksdb::Options::default(), path.clone()) {
274 Ok(()) => return Ok(()),
275 Err(err) => {
276 if std::time::Instant::now() >= deadline {
277 return Err(err);
278 }
279 std::thread::sleep(Duration::from_millis(100));
280 }
281 }
282 }
283 })
284}
285
286#[cfg(not(msim))]
288pub async fn safe_drop_db(path: PathBuf, timeout: Duration) -> Result<(), rocksdb::Error> {
289 let mut backoff = backoff::ExponentialBackoff {
290 max_elapsed_time: Some(timeout),
291 ..Default::default()
292 };
293 loop {
294 match rocksdb::DB::destroy(&rocksdb::Options::default(), path.clone()) {
295 Ok(()) => return Ok(()),
296 Err(err) => match backoff.next_backoff() {
297 Some(duration) => tokio::time::sleep(duration).await,
298 None => return Err(err),
299 },
300 }
301 }
302}
303
304fn populate_missing_cfs(
308 input_cfs: &[(&str, rocksdb::Options)],
309 path: &Path,
310) -> Result<Vec<(String, rocksdb::Options)>, rocksdb::Error> {
311 let mut cfs = vec![];
312 let input_cf_index: HashSet<_> = input_cfs.iter().map(|(name, _)| *name).collect();
313 let existing_cfs =
314 rocksdb::DBWithThreadMode::<MultiThreaded>::list_cf(&rocksdb::Options::default(), path)
315 .ok()
316 .unwrap_or_default();
317
318 let default_db_options = default_db_options();
319 let mut names_default = false;
320 for cf_name in existing_cfs {
321 names_default |= cf_name == rocksdb::DEFAULT_COLUMN_FAMILY_NAME;
322 if !input_cf_index.contains(&cf_name[..]) {
323 cfs.push((cf_name, default_db_options.options.clone()));
324 }
325 }
326 if !names_default && !input_cf_index.contains(rocksdb::DEFAULT_COLUMN_FAMILY_NAME) {
330 cfs.push((
331 rocksdb::DEFAULT_COLUMN_FAMILY_NAME.to_owned(),
332 default_db_options.options,
333 ));
334 }
335 cfs.extend(
336 input_cfs
337 .iter()
338 .map(|(name, opts)| (name.to_string(), (*opts).clone())),
339 );
340 Ok(cfs)
341}
342
343impl<K, V> DBMap<K, V> {
347 fn get_rocksdb_int_property(
348 rocksdb: &RocksDB,
349 cf: &impl AsColumnFamilyRef,
350 property_name: &CStr,
351 ) -> Result<i64, TypedStoreError> {
352 match rocksdb.underlying.property_int_value_cf(cf, property_name) {
353 Ok(Some(value)) => Ok(value.min(i64::MAX as u64).try_into().unwrap_or_default()),
354 Ok(None) => Ok(0),
355 Err(e) => Err(TypedStoreError::RocksDB(e.into_string())),
356 }
357 }
358
359 pub fn estimated_len(&self) -> Result<u64, TypedStoreError> {
365 let Storage::Rocks(rocksdb) = &self.db.storage else {
366 return Ok(0);
367 };
368 let Some(cf) = rocksdb.underlying.cf_handle(self.cf_name()) else {
369 return Ok(0);
370 };
371 Ok(
372 Self::get_rocksdb_int_property(rocksdb, &cf, properties::ESTIMATE_NUM_KEYS)?.max(0)
373 as u64,
374 )
375 }
376
377 pub(crate) fn report_rocksdb_metrics(
378 database: &Arc<Database>,
379 cf_name: &str,
380 db_metrics: &Arc<DBMetrics>,
381 ) {
382 let Storage::Rocks(rocksdb) = &database.storage else {
383 return;
384 };
385
386 let Some(cf) = rocksdb.underlying.cf_handle(cf_name) else {
387 warn!(
388 "unable to report metrics for cf {cf_name:?} in db {:?}",
389 database.db_name()
390 );
391 return;
392 };
393
394 db_metrics
395 .cf_metrics
396 .rocksdb_total_sst_files_size
397 .with_label_values(&[cf_name])
398 .set(
399 Self::get_rocksdb_int_property(rocksdb, &cf, properties::TOTAL_SST_FILES_SIZE)
400 .unwrap_or(METRICS_ERROR),
401 );
402 db_metrics
403 .cf_metrics
404 .rocksdb_total_blob_files_size
405 .with_label_values(&[cf_name])
406 .set(
407 Self::get_rocksdb_int_property(
408 rocksdb,
409 &cf,
410 ROCKSDB_PROPERTY_TOTAL_BLOB_FILES_SIZE,
411 )
412 .unwrap_or(METRICS_ERROR),
413 );
414 let total_num_files: i64 = (0..=6)
418 .map(|level| {
419 Self::get_rocksdb_int_property(rocksdb, &cf, &num_files_at_level(level))
420 .unwrap_or(METRICS_ERROR)
421 })
422 .sum();
423 db_metrics
424 .cf_metrics
425 .rocksdb_total_num_files
426 .with_label_values(&[cf_name])
427 .set(total_num_files);
428 db_metrics
429 .cf_metrics
430 .rocksdb_num_level0_files
431 .with_label_values(&[cf_name])
432 .set(
433 Self::get_rocksdb_int_property(rocksdb, &cf, &num_files_at_level(0))
434 .unwrap_or(METRICS_ERROR),
435 );
436 db_metrics
437 .cf_metrics
438 .rocksdb_current_size_active_mem_tables
439 .with_label_values(&[cf_name])
440 .set(
441 Self::get_rocksdb_int_property(rocksdb, &cf, properties::CUR_SIZE_ACTIVE_MEM_TABLE)
442 .unwrap_or(METRICS_ERROR),
443 );
444 db_metrics
445 .cf_metrics
446 .rocksdb_size_all_mem_tables
447 .with_label_values(&[cf_name])
448 .set(
449 Self::get_rocksdb_int_property(rocksdb, &cf, properties::SIZE_ALL_MEM_TABLES)
450 .unwrap_or(METRICS_ERROR),
451 );
452 db_metrics
453 .cf_metrics
454 .rocksdb_num_snapshots
455 .with_label_values(&[cf_name])
456 .set(
457 Self::get_rocksdb_int_property(rocksdb, &cf, properties::NUM_SNAPSHOTS)
458 .unwrap_or(METRICS_ERROR),
459 );
460 db_metrics
461 .cf_metrics
462 .rocksdb_oldest_snapshot_time
463 .with_label_values(&[cf_name])
464 .set(
465 Self::get_rocksdb_int_property(rocksdb, &cf, properties::OLDEST_SNAPSHOT_TIME)
466 .unwrap_or(METRICS_ERROR),
467 );
468 db_metrics
469 .cf_metrics
470 .rocksdb_actual_delayed_write_rate
471 .with_label_values(&[cf_name])
472 .set(
473 Self::get_rocksdb_int_property(rocksdb, &cf, properties::ACTUAL_DELAYED_WRITE_RATE)
474 .unwrap_or(METRICS_ERROR),
475 );
476 db_metrics
477 .cf_metrics
478 .rocksdb_is_write_stopped
479 .with_label_values(&[cf_name])
480 .set(
481 Self::get_rocksdb_int_property(rocksdb, &cf, properties::IS_WRITE_STOPPED)
482 .unwrap_or(METRICS_ERROR),
483 );
484 db_metrics
485 .cf_metrics
486 .rocksdb_block_cache_capacity
487 .with_label_values(&[cf_name])
488 .set(
489 Self::get_rocksdb_int_property(rocksdb, &cf, properties::BLOCK_CACHE_CAPACITY)
490 .unwrap_or(METRICS_ERROR),
491 );
492 db_metrics
493 .cf_metrics
494 .rocksdb_block_cache_usage
495 .with_label_values(&[cf_name])
496 .set(
497 Self::get_rocksdb_int_property(rocksdb, &cf, properties::BLOCK_CACHE_USAGE)
498 .unwrap_or(METRICS_ERROR),
499 );
500 db_metrics
501 .cf_metrics
502 .rocksdb_block_cache_pinned_usage
503 .with_label_values(&[cf_name])
504 .set(
505 Self::get_rocksdb_int_property(rocksdb, &cf, properties::BLOCK_CACHE_PINNED_USAGE)
506 .unwrap_or(METRICS_ERROR),
507 );
508 db_metrics
509 .cf_metrics
510 .rocksdb_estimate_table_readers_mem
511 .with_label_values(&[cf_name])
512 .set(
513 Self::get_rocksdb_int_property(
514 rocksdb,
515 &cf,
516 properties::ESTIMATE_TABLE_READERS_MEM,
517 )
518 .unwrap_or(METRICS_ERROR),
519 );
520 db_metrics
521 .cf_metrics
522 .rocksdb_estimated_num_keys
523 .with_label_values(&[cf_name])
524 .set(
525 Self::get_rocksdb_int_property(rocksdb, &cf, properties::ESTIMATE_NUM_KEYS)
526 .unwrap_or(METRICS_ERROR),
527 );
528 db_metrics
529 .cf_metrics
530 .rocksdb_num_immutable_mem_tables
531 .with_label_values(&[cf_name])
532 .set(
533 Self::get_rocksdb_int_property(rocksdb, &cf, properties::NUM_IMMUTABLE_MEM_TABLE)
534 .unwrap_or(METRICS_ERROR),
535 );
536 db_metrics
537 .cf_metrics
538 .rocksdb_mem_table_flush_pending
539 .with_label_values(&[cf_name])
540 .set(
541 Self::get_rocksdb_int_property(rocksdb, &cf, properties::MEM_TABLE_FLUSH_PENDING)
542 .unwrap_or(METRICS_ERROR),
543 );
544 db_metrics
545 .cf_metrics
546 .rocksdb_compaction_pending
547 .with_label_values(&[cf_name])
548 .set(
549 Self::get_rocksdb_int_property(rocksdb, &cf, properties::COMPACTION_PENDING)
550 .unwrap_or(METRICS_ERROR),
551 );
552 db_metrics
553 .cf_metrics
554 .rocksdb_estimate_pending_compaction_bytes
555 .with_label_values(&[cf_name])
556 .set(
557 Self::get_rocksdb_int_property(
558 rocksdb,
559 &cf,
560 properties::ESTIMATE_PENDING_COMPACTION_BYTES,
561 )
562 .unwrap_or(METRICS_ERROR),
563 );
564 db_metrics
565 .cf_metrics
566 .rocksdb_num_running_compactions
567 .with_label_values(&[cf_name])
568 .set(
569 Self::get_rocksdb_int_property(rocksdb, &cf, properties::NUM_RUNNING_COMPACTIONS)
570 .unwrap_or(METRICS_ERROR),
571 );
572 db_metrics
573 .cf_metrics
574 .rocksdb_num_running_flushes
575 .with_label_values(&[cf_name])
576 .set(
577 Self::get_rocksdb_int_property(rocksdb, &cf, properties::NUM_RUNNING_FLUSHES)
578 .unwrap_or(METRICS_ERROR),
579 );
580 db_metrics
581 .cf_metrics
582 .rocksdb_estimate_oldest_key_time
583 .with_label_values(&[cf_name])
584 .set(
585 Self::get_rocksdb_int_property(rocksdb, &cf, properties::ESTIMATE_OLDEST_KEY_TIME)
586 .unwrap_or(METRICS_ERROR),
587 );
588 db_metrics
589 .cf_metrics
590 .rocksdb_background_errors
591 .with_label_values(&[cf_name])
592 .set(
593 Self::get_rocksdb_int_property(rocksdb, &cf, properties::BACKGROUND_ERRORS)
594 .unwrap_or(METRICS_ERROR),
595 );
596 db_metrics
597 .cf_metrics
598 .rocksdb_base_level
599 .with_label_values(&[cf_name])
600 .set(
601 Self::get_rocksdb_int_property(rocksdb, &cf, properties::BASE_LEVEL)
602 .unwrap_or(METRICS_ERROR),
603 );
604 }
605}