1use std::{
2 ops::Deref,
3 path::{Path, PathBuf},
4 sync::Arc,
5};
6
7use crate::io;
8use crate::must_ok;
9use crate::utils::merge_operator::MergeOperator;
10#[cfg(feature = "metrics")]
11use crate::utils::observe::{NoopObserver, Observer};
12use serde::{Deserialize, Serialize};
13
14use super::OpCode;
15
16pub(crate) fn dir_parent_for_sync(path: &Path) -> Option<&Path> {
17 match path.parent() {
18 Some(parent) if parent.as_os_str().is_empty() => Some(Path::new(".")),
19 Some(parent) => Some(parent),
20 None => None,
21 }
22}
23
24fn create_dir_all_and_sync(fs: &dyn io::FileSystem, path: &Path) -> std::io::Result<()> {
25 if fs.try_exists(path)? {
26 return Ok(());
27 }
28
29 let mut created = Vec::new();
30 let mut cursor = Some(path);
31 while let Some(dir) = cursor {
32 if fs.try_exists(dir)? {
33 break;
34 }
35 created.push(dir.to_path_buf());
36 cursor = dir.parent();
37 }
38
39 fs.create_dir_all(path)?;
40
41 created.reverse();
42 for dir in created {
43 if let Some(parent) = dir_parent_for_sync(&dir) {
44 fs.sync_dir(parent)?;
45 }
46 }
47 Ok(())
48}
49
50#[derive(Clone)]
52pub struct Options {
53 pub sync_on_write: bool,
65 pub concurrent_write: u8,
75 pub gc_timeout: u64,
82 pub checkpoint_nudge_ms: u64,
89 pub data_garbage_ratio: u32,
91 pub gc_eager: bool,
93 pub blob_file_size: usize,
95 pub blob_garbage_ratio: u32,
97 pub tmp_store: bool,
101 pub(crate) db_root: PathBuf,
103 pub log_root: PathBuf,
107 pub lru_capacity: usize,
115 pub stat_mask_cache_count: usize,
117 pub data_handle_cache_capacity: usize,
119 pub blob_handle_cache_capacity: usize,
121 pub data_file_size: usize,
123 pub wal_buffer_size: usize,
126 pub max_ckpt_per_txn: usize,
130 pub wal_file_size: u32,
132 pub truncate_corrupted_wal: bool,
138 pub sync_merge_window_us: u64,
140 #[cfg(feature = "metrics")]
143 pub observer: Arc<dyn Observer>,
144 pub(crate) fs: Arc<dyn io::FileSystem>,
148}
149
150#[derive(Clone, Copy, Debug, PartialEq, Eq)]
151#[repr(C)]
152pub struct PersistedBucketOptions {
153 pub cache_capacity: usize,
155 pub cache_evict_pct: usize,
157 pub pool_capacity: usize,
164 pub checkpoint_size: usize,
166 pub inline_size: usize,
171 pub split_elems: u16,
173 pub consolidate_threshold: u16,
175 pub enable_compression: bool,
177 pub enable_backpressure: bool,
179 pub _padding: [u8; 2],
180}
181
182impl From<&BucketOptions> for PersistedBucketOptions {
183 fn from(opt: &BucketOptions) -> Self {
184 Self {
185 cache_capacity: opt.cache_capacity,
186 cache_evict_pct: opt.cache_evict_pct,
187 pool_capacity: opt.pool_capacity,
188 checkpoint_size: opt.checkpoint_size,
189 inline_size: opt.inline_size,
190 split_elems: opt.split_elems,
191 consolidate_threshold: opt.consolidate_threshold,
192 enable_compression: opt.enable_compression,
193 enable_backpressure: opt.enable_backpressure,
194 _padding: [0u8; 2],
195 }
196 }
197}
198
199impl PersistedBucketOptions {
200 pub(crate) fn to_runtime(self) -> BucketOptions {
201 let mut opt = BucketOptions::new();
202 opt.cache_capacity = self.cache_capacity;
203 opt.cache_evict_pct = self.cache_evict_pct;
204 opt.pool_capacity = self.pool_capacity;
205 opt.checkpoint_size = self.checkpoint_size;
206 opt.inline_size = self.inline_size;
207 opt.split_elems = self.split_elems;
208 opt.consolidate_threshold = self.consolidate_threshold;
209 opt.enable_compression = self.enable_compression;
210 opt.enable_backpressure = self.enable_backpressure;
211 opt
212 }
213}
214
215#[derive(Clone)]
220pub struct BucketOptions {
221 pub cache_capacity: usize,
223 pub cache_evict_pct: usize,
225 pub pool_capacity: usize,
232 pub checkpoint_size: usize,
234 pub inline_size: usize,
239 pub split_elems: u16,
241 pub consolidate_threshold: u16,
243 pub enable_compression: bool,
245 pub enable_backpressure: bool,
247 pub merge_operator: Option<Arc<dyn MergeOperator>>,
249}
250
251impl Default for BucketOptions {
252 fn default() -> Self {
253 Self::new()
254 }
255}
256
257impl std::fmt::Debug for BucketOptions {
258 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259 let mut d = f.debug_struct("BucketOptions");
260 d.field("cache_capacity", &self.cache_capacity)
261 .field("cache_evict_pct", &self.cache_evict_pct)
262 .field("pool_capacity", &self.pool_capacity)
263 .field("checkpoint_size", &self.checkpoint_size)
264 .field("inline_size", &self.inline_size)
265 .field("split_elems", &self.split_elems)
266 .field("consolidate_threshold", &self.consolidate_threshold)
267 .field("enable_compression", &self.enable_compression)
268 .field("enable_backpressure", &self.enable_backpressure)
269 .field("merge_operator", &self.merge_operator.is_some());
270 d.finish()
271 }
272}
273
274impl BucketOptions {
275 pub const MIN_SPLIT_ELEMS: u16 = 64;
276 pub const MAX_SPLIT_ELEMS: u16 = 512;
277 pub const CACHE_CAP: usize = 1 << 30; pub const POOL_CAP: usize = 1 << 30; pub const CHECKPOINT_SIZE: usize = 256 << 20; pub const MIN_INLINE_SIZE: usize = 256;
281 pub const INLINE_SIZE: usize = 1024;
282 pub const MAX_INLINE_SIZE: usize = 4096;
283
284 pub fn new() -> Self {
285 Self {
286 cache_capacity: Self::CACHE_CAP,
287 cache_evict_pct: 20,
288 pool_capacity: Self::POOL_CAP,
289 checkpoint_size: Self::CHECKPOINT_SIZE,
290 consolidate_threshold: Self::MAX_SPLIT_ELEMS / 2,
291 inline_size: Self::INLINE_SIZE,
292 split_elems: Self::MAX_SPLIT_ELEMS,
293 enable_compression: false,
294 enable_backpressure: true,
295 merge_operator: None,
296 }
297 }
298
299 pub fn validate(mut self) -> BucketOptions {
300 if self.checkpoint_size == 0 {
301 self.checkpoint_size = Self::CHECKPOINT_SIZE;
302 }
303 if self.pool_capacity == 0 {
304 self.pool_capacity = Self::POOL_CAP;
305 }
306 if self.cache_capacity == 0 {
307 self.cache_capacity = Self::CACHE_CAP;
308 }
309 if self.checkpoint_size > self.pool_capacity {
310 self.checkpoint_size = self.pool_capacity;
311 }
312 self.cache_evict_pct = self.cache_evict_pct.clamp(10, 80);
313 self.split_elems = self
314 .split_elems
315 .clamp(Self::MIN_SPLIT_ELEMS, Self::MAX_SPLIT_ELEMS);
316 self.consolidate_threshold = self.consolidate_threshold.clamp(16, self.split_elems / 2);
317 self.inline_size = self
318 .inline_size
319 .clamp(Self::MIN_INLINE_SIZE, Self::MAX_INLINE_SIZE);
320 self
321 }
322}
323
324impl Options {
325 pub const CONCURRENT_WRITE: u8 = 16;
326 pub const MAX_CONCURRENT_WRITE: u8 = 128;
327 pub const DATA_FILE_SIZE: usize = 64 << 20; pub const BLOB_FILE_SIZE: usize = 256 << 20; pub const LRU_CAPACITY: usize = 256 << 20; pub const STAT_MASK_CACHE_CNT: usize = 16384;
332 pub const WAL_BUF_SZ: usize = 16 << 20; pub const WAL_FILE_SZ: usize = 64 << 20; pub(crate) const MAX_KEY_SIZE: usize = 64 << 10;
336 pub(crate) const MAX_KV_SIZE: usize = 1 << 30; pub fn new<P: AsRef<Path>>(db_root: P) -> Self {
340 Self {
341 sync_on_write: true,
342 concurrent_write: Self::CONCURRENT_WRITE,
343 tmp_store: false,
344 gc_timeout: 60 * 1000, checkpoint_nudge_ms: 60 * 1000, data_garbage_ratio: 20, gc_eager: true,
348 blob_file_size: Self::BLOB_FILE_SIZE,
349 blob_garbage_ratio: 50, db_root: db_root.as_ref().to_path_buf(),
351 log_root: db_root.as_ref().to_path_buf(),
352 lru_capacity: Self::LRU_CAPACITY,
353 stat_mask_cache_count: Self::STAT_MASK_CACHE_CNT,
354 data_handle_cache_capacity: 128,
355 blob_handle_cache_capacity: 128,
356 data_file_size: Self::DATA_FILE_SIZE,
357 wal_buffer_size: Self::WAL_BUF_SZ,
358 max_ckpt_per_txn: 1_000_000, wal_file_size: Self::WAL_FILE_SZ as u32,
360 truncate_corrupted_wal: true,
361 sync_merge_window_us: 125,
362 #[cfg(feature = "metrics")]
363 observer: Arc::new(NoopObserver),
364 fs: Arc::new(io::OsFileSystem),
365 }
366 }
367
368 pub fn validate(mut self) -> Result<ParsedOptions, OpCode> {
370 self.concurrent_write = self
371 .concurrent_write
372 .clamp(1, Self::MAX_CONCURRENT_WRITE)
373 .next_power_of_two();
374 if self.stat_mask_cache_count == 0 {
375 self.stat_mask_cache_count = Self::STAT_MASK_CACHE_CNT;
376 }
377 if self.lru_capacity == 0 {
378 self.lru_capacity = Self::LRU_CAPACITY;
379 }
380 if self.data_file_size == 0 {
381 self.data_file_size = Self::DATA_FILE_SIZE;
382 }
383 if self.blob_file_size == 0 {
384 self.blob_file_size = Self::BLOB_FILE_SIZE;
385 }
386
387 self.create_dir().map_err(|e| {
388 eprintln!("create dir fail {e:?}");
389 OpCode::IoError
390 })?;
391 Ok(ParsedOptions { inner: self })
392 }
393
394 pub fn create_dir(&self) -> std::io::Result<()> {
396 let (db_root, data_root, log_root) = (self.db_root(), self.data_root(), self.log_root());
397
398 if !self.fs.try_exists(&db_root)? {
399 create_dir_all_and_sync(self.fs.as_ref(), &db_root)?;
400 }
401 if !self.fs.try_exists(&data_root)? {
402 create_dir_all_and_sync(self.fs.as_ref(), &data_root)?;
403 }
404 if !self.fs.try_exists(&log_root)? {
405 create_dir_all_and_sync(self.fs.as_ref(), &log_root)?;
406 }
407 Ok(())
408 }
409}
410
411pub struct ParsedOptions {
412 pub(crate) inner: Options,
413}
414
415impl Deref for ParsedOptions {
416 type Target = Options;
417 fn deref(&self) -> &Self::Target {
418 &self.inner
419 }
420}
421
422impl Options {
423 pub const SEP: &'static str = "_";
424 pub const DATA_PREFIX: &'static str = "data";
425 pub const BLOB_PREFIX: &'static str = "blob";
426 pub const WAL_PREFIX: &'static str = "wal";
427 pub const GROUP_WAL_PREFIX: &'static str = "group_wal";
428 pub const MANIFEST: &'static str = "manifest";
429 pub const SHARED_ID: u8 = 255;
431
432 pub fn data_root(&self) -> PathBuf {
433 self.db_root().join("data")
434 }
435
436 pub fn data_file(&self, id: u64) -> PathBuf {
437 self.data_root()
438 .join(format!("{}{}{}", Self::DATA_PREFIX, Self::SEP, id))
439 }
440
441 pub fn blob_file(&self, id: u64) -> PathBuf {
442 self.data_root()
443 .join(format!("{}{}{}", Self::BLOB_PREFIX, Self::SEP, id))
444 }
445
446 pub fn log_root(&self) -> PathBuf {
447 if self.log_root == self.db_root {
448 self.db_root.join("log")
449 } else {
450 self.log_root.clone()
451 }
452 }
453
454 pub fn db_root(&self) -> PathBuf {
455 self.db_root.clone()
456 }
457
458 pub fn wal_file(&self, group_id: u8, seq: u64) -> PathBuf {
459 self.log_root().join(format!(
460 "{}{}{}{}{}",
461 Self::WAL_PREFIX,
462 Self::SEP,
463 group_id,
464 Self::SEP,
465 seq
466 ))
467 }
468
469 pub fn group_wal_file(&self, seq: u64) -> PathBuf {
471 self.log_root()
472 .join(format!("{}{}{}", Self::GROUP_WAL_PREFIX, Self::SEP, seq))
473 }
474
475 pub fn physical_wal_path(&self, physical_wal_id: u8, seq: u64) -> PathBuf {
477 if physical_wal_id == Self::SHARED_ID {
478 self.group_wal_file(seq)
479 } else {
480 self.wal_file(physical_wal_id, seq)
481 }
482 }
483
484 pub fn manifest(&self) -> PathBuf {
485 self.log_root().join(Self::MANIFEST)
486 }
487
488 pub(crate) fn sync_data_dir(&self) {
489 must_ok!(
490 self.try_sync_data_dir(),
491 "can't sync the data directory {:?}",
492 self.data_root()
493 );
494 }
495
496 pub(crate) fn try_sync_data_dir(&self) -> Result<(), OpCode> {
500 self.fs
501 .as_ref()
502 .sync_dir(&self.data_root())
503 .map_err(OpCode::from)
504 }
505
506 pub(crate) fn sync_log_dir(&self) {
507 must_ok!(
508 self.try_sync_log_dir(),
509 "can't sync the log directory {:?}",
510 self.log_root()
511 );
512 }
513
514 pub(crate) fn try_sync_log_dir(&self) -> Result<(), OpCode> {
518 self.fs
519 .as_ref()
520 .sync_dir(&self.log_root())
521 .map_err(OpCode::from)
522 }
523
524 pub(crate) fn has_legacy_per_group_wal(&self) -> Result<bool, OpCode> {
529 let prefix = format!("{}{}", Self::WAL_PREFIX, Self::SEP);
530 for entry in self.fs.read_dir(&self.log_root())? {
531 let Some(name) = entry.file_name() else {
532 continue;
533 };
534 let Some(raw) = name.to_str() else {
535 continue;
536 };
537 if raw.starts_with(&prefix) {
538 return Ok(true);
539 }
540 }
541 Ok(false)
542 }
543}
544
545#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
546#[serde(default)]
547pub(crate) struct PersistedOptions {
548 pub concurrent_write: u8,
549 pub sync_on_write: bool,
550 pub gc_timeout: u64,
551 pub checkpoint_nudge_ms: u64,
552 pub data_garbage_ratio: u32,
553 pub gc_eager: bool,
554 pub blob_file_size: usize,
555 pub blob_garbage_ratio: u32,
556 pub lru_capacity: usize,
557 pub stat_mask_cache_count: usize,
558 pub data_handle_cache_capacity: usize,
559 pub blob_handle_cache_capacity: usize,
560 pub data_file_size: usize,
561 pub wal_buffer_size: usize,
562 pub max_ckpt_per_txn: usize,
563 pub wal_file_size: u32,
564 pub truncate_corrupted_wal: bool,
565}
566
567impl Default for PersistedOptions {
568 fn default() -> Self {
569 Self {
570 concurrent_write: Options::CONCURRENT_WRITE,
571 sync_on_write: true,
572 gc_timeout: 60 * 1000,
573 checkpoint_nudge_ms: 60 * 1000,
574 data_garbage_ratio: 20,
575 gc_eager: true,
576 blob_file_size: Options::BLOB_FILE_SIZE,
577 blob_garbage_ratio: 50,
578 lru_capacity: Options::LRU_CAPACITY,
579 stat_mask_cache_count: Options::STAT_MASK_CACHE_CNT,
580 data_handle_cache_capacity: 128,
581 blob_handle_cache_capacity: 128,
582 data_file_size: Options::DATA_FILE_SIZE,
583 wal_buffer_size: Options::WAL_BUF_SZ,
584 max_ckpt_per_txn: 1_000_000,
585 wal_file_size: Options::WAL_FILE_SZ as u32,
586 truncate_corrupted_wal: true,
587 }
588 }
589}
590
591impl PersistedOptions {
592 pub(crate) fn from_options(opt: &Options) -> Self {
593 Self {
594 concurrent_write: opt.concurrent_write,
595 sync_on_write: opt.sync_on_write,
596 gc_timeout: opt.gc_timeout,
597 checkpoint_nudge_ms: opt.checkpoint_nudge_ms,
598 data_garbage_ratio: opt.data_garbage_ratio,
599 gc_eager: opt.gc_eager,
600 blob_file_size: opt.blob_file_size,
601 blob_garbage_ratio: opt.blob_garbage_ratio,
602 lru_capacity: opt.lru_capacity,
603 stat_mask_cache_count: opt.stat_mask_cache_count,
604 data_handle_cache_capacity: opt.data_handle_cache_capacity,
605 blob_handle_cache_capacity: opt.blob_handle_cache_capacity,
606 data_file_size: opt.data_file_size,
607 wal_buffer_size: opt.wal_buffer_size,
608 max_ckpt_per_txn: opt.max_ckpt_per_txn,
609 wal_file_size: opt.wal_file_size,
610 truncate_corrupted_wal: opt.truncate_corrupted_wal,
611 }
612 }
613
614 pub(crate) fn from_json(buf: &[u8]) -> Result<Self, OpCode> {
615 serde_json::from_slice(buf).map_err(|_| OpCode::Corruption)
616 }
617
618 pub(crate) fn to_json(&self) -> Result<Vec<u8>, OpCode> {
619 serde_json::to_vec(self).map_err(|_| OpCode::Corruption)
620 }
621}
622
623impl Drop for ParsedOptions {
624 fn drop(&mut self) {
625 if self.inner.tmp_store {
626 log::info!("remove db_root {:?}", self.inner.db_root);
627 let _ = std::fs::remove_dir_all(&self.inner.db_root);
628 }
629 }
630}
631
632#[cfg(test)]
633mod tests {
634 use std::{io::ErrorKind, sync::Arc};
635
636 use crate::{
637 OpCode, RandomPath,
638 io::testfs::{InjectOp, InjectedFileSystem},
639 };
640
641 use super::{Options, PersistedOptions};
642
643 #[test]
644 fn validate_returns_io_error_when_create_dir_all_fails() {
645 let root = RandomPath::tmp();
646 let mut opt = Options::new(&*root);
647 let fs = Arc::new(InjectedFileSystem::new());
648 fs.fail_once(
649 InjectOp::CreateDirAll,
650 opt.db_root(),
651 ErrorKind::PermissionDenied,
652 );
653 opt.fs = fs;
654 let err = opt.validate().err().expect("validate must fail");
655 assert_eq!(err, OpCode::IoError);
656 }
657
658 #[test]
659 fn try_sync_data_dir_propagates_a_directory_sync_failure() {
660 let root = RandomPath::tmp();
661 let mut opt = Options::new(&*root);
662 let fs = Arc::new(InjectedFileSystem::new());
663 opt.fs = fs.clone();
664 let parsed = opt.validate().expect("options must validate");
665 fs.fail_once(
667 InjectOp::SyncDir,
668 parsed.data_root(),
669 ErrorKind::PermissionDenied,
670 );
671
672 assert_eq!(
673 parsed.try_sync_data_dir(),
674 Err(OpCode::IoError),
675 "the open path must see a failed data-directory sync as an error"
676 );
677 assert_eq!(
678 parsed.try_sync_log_dir(),
679 Ok(()),
680 "the failure must be the data directory's, not a device-wide one"
681 );
682 }
683
684 #[test]
685 fn validate_returns_io_error_when_parent_sync_fails() {
686 let root = RandomPath::tmp();
687 let mut opt = Options::new(&*root);
688 let parent = opt
689 .db_root()
690 .parent()
691 .expect("db_root must have parent")
692 .to_path_buf();
693 let fs = Arc::new(InjectedFileSystem::new());
694 fs.fail_once(InjectOp::SyncDir, parent, ErrorKind::PermissionDenied);
695 opt.fs = fs;
696 let err = opt.validate().err().expect("validate must fail");
697 assert_eq!(err, OpCode::IoError);
698 }
699
700 #[test]
701 fn persisted_options_fill_missing_fields_with_defaults() {
702 let json = br#"{"concurrent_write":8}"#;
703 let opt = PersistedOptions::from_json(json).expect("decode persisted options");
704 let expected = PersistedOptions {
705 concurrent_write: 8,
706 ..Default::default()
707 };
708 assert_eq!(opt, expected);
709 }
710
711 #[test]
712 fn persisted_options_ignore_removed_fields() {
713 let json = br#"{"concurrent_write":16,"old_field":true}"#;
714 let opt = PersistedOptions::from_json(json).expect("decode persisted options");
715 let expected = PersistedOptions {
716 concurrent_write: 16,
717 ..Default::default()
718 };
719 assert_eq!(opt, expected);
720 }
721
722 #[test]
723 fn persisted_options_keep_a_foreign_concurrent_write() {
724 let root = RandomPath::tmp();
727 let mut opt = Options::new(&*root);
728 opt.concurrent_write = 8;
729 let persisted = PersistedOptions::from_options(&opt.validate().unwrap());
730
731 let json = persisted.to_json().expect("encode persisted options");
732 let decoded = PersistedOptions::from_json(&json).expect("decode persisted options");
733 assert_eq!(decoded.concurrent_write, 8);
734 }
735}