use crate::error::{DbError, DbResult};
pub(crate) const DEFAULT_COMPACTION_MAX_FILES_PER_PASS: usize = 4;
#[cfg(any(feature = "armour", feature = "postcard-codec"))]
fn default_compaction_max_files_per_pass() -> usize {
DEFAULT_COMPACTION_MAX_FILES_PER_PASS
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(
any(feature = "armour", feature = "postcard-codec"),
derive(serde::Deserialize)
)]
pub enum IoBackend {
Uring { sqpoll_idle_ms: Option<u32> },
#[default]
Pwrite,
}
#[derive(Debug, Clone)]
#[cfg_attr(
any(feature = "armour", feature = "postcard-codec"),
derive(serde::Deserialize)
)]
pub struct Config {
pub shard_count: usize,
pub max_file_size: u64,
pub compaction_threshold: f64,
#[cfg_attr(
any(feature = "armour", feature = "postcard-codec"),
serde(default = "default_compaction_max_files_per_pass")
)]
pub compaction_max_files_per_pass: usize,
pub enable_fsync: bool,
pub write_buffer_size: usize,
#[cfg(feature = "var-collections")]
pub block_cache: CacheConfig,
#[cfg(feature = "var-collections")]
pub value_cache: CacheConfig,
pub shard_prefix_bits: usize,
pub reversed: bool,
#[cfg_attr(any(feature = "armour", feature = "postcard-codec"), serde(default))]
pub iterable: bool,
#[cfg_attr(any(feature = "armour", feature = "postcard-codec"), serde(default))]
pub hints: Option<bool>,
pub direct_io: bool,
#[cfg_attr(any(feature = "armour", feature = "postcard-codec"), serde(default))]
pub io_backend: IoBackend,
#[cfg(feature = "encryption")]
pub encryption_key: Option<[u8; 32]>,
}
#[cfg(feature = "var-collections")]
#[derive(Debug, Clone)]
#[cfg_attr(
any(feature = "armour", feature = "postcard-codec"),
derive(serde::Deserialize)
)]
pub struct CacheConfig {
pub max_size: u64,
pub estimated_items: usize,
}
impl Config {
pub fn test() -> Self {
Config::balanced().shard_count(2).hints(true).build()
}
#[doc(hidden)]
pub fn test_no_hints() -> Self {
let mut cfg = Self::test();
cfg.hints = None;
cfg
}
pub(crate) fn with_resolved_hints(mut self, default: bool) -> Self {
self.hints = Some(self.hints.unwrap_or(default));
self
}
#[cfg(feature = "var-collections")]
pub(crate) fn var_cache_warn_needed(&self) -> bool {
self.block_cache.max_size == 0 && self.value_cache.max_size == 0
}
#[cfg(feature = "var-collections")]
pub(crate) fn app_cache_without_direct_io(&self) -> bool {
(self.block_cache.max_size > 0 || self.value_cache.max_size > 0) && !self.direct_io
}
pub fn validate(&self) -> DbResult<()> {
if self.shard_count == 0 || self.shard_count > 255 {
return Err(DbError::Config("shard_count must be between 1 and 255"));
}
if self.max_file_size < 4096 {
return Err(DbError::Config("max_file_size must be at least 4096"));
}
if self.write_buffer_size < 4096 {
return Err(DbError::Config("write_buffer_size must be at least 4096"));
}
if self.max_file_size > u32::MAX as u64 {
return Err(DbError::Config(
"max_file_size must not exceed u32::MAX (4 GiB)",
));
}
if (self.write_buffer_size as u64) > (u32::MAX as u64) - 4096 {
return Err(DbError::Config(
"write_buffer_size must not exceed u32::MAX - 4096",
));
}
if (self.write_buffer_size as u64) > self.max_file_size {
return Err(DbError::Config(
"max_file_size must be >= write_buffer_size",
));
}
if self.shard_prefix_bits > u8::MAX as usize {
return Err(DbError::Config("shard_prefix_bits must be <= 255"));
}
if !self.compaction_threshold.is_finite()
|| !(0.0..=1.0).contains(&self.compaction_threshold)
{
return Err(DbError::Config(
"compaction_threshold must be a finite value in [0.0, 1.0]",
));
}
let page_aligned = {
#[cfg(feature = "encryption")]
{
self.direct_io || self.encryption_key.is_some()
}
#[cfg(not(feature = "encryption"))]
{
self.direct_io
}
};
if page_aligned {
if self.write_buffer_size < 8192 {
return Err(DbError::Config(
"write_buffer_size must be at least 8192 when direct_io or encryption is enabled",
));
}
if !self.write_buffer_size.is_multiple_of(4096) {
return Err(DbError::Config(
"write_buffer_size must be a multiple of 4096 when direct_io or encryption is enabled",
));
}
}
Ok(())
}
}
#[cfg(feature = "var-collections")]
impl Default for CacheConfig {
fn default() -> Self {
Self {
max_size: 0,
estimated_items: 100_000,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Workload {
Balanced,
LowLatency,
HighThroughput,
Durable,
LargeValues,
SmallValues,
}
pub(crate) fn default_shard_count() -> usize {
#[cfg(debug_assertions)]
{
std::thread::available_parallelism()
.map(|p| p.get() / 2)
.unwrap_or(2)
.clamp(1, 4)
}
#[cfg(not(debug_assertions))]
{
std::thread::available_parallelism()
.map(|p| p.get() / 2)
.unwrap_or(4)
.clamp(1, 8)
}
}
impl Workload {
fn base(self) -> Config {
let common = Config {
shard_count: default_shard_count(),
max_file_size: 256 * 1024 * 1024,
compaction_threshold: 0.30,
compaction_max_files_per_pass: DEFAULT_COMPACTION_MAX_FILES_PER_PASS,
enable_fsync: false,
write_buffer_size: 1024 * 1024,
#[cfg(feature = "var-collections")]
block_cache: CacheConfig {
max_size: 0,
estimated_items: 100_000,
},
#[cfg(feature = "var-collections")]
value_cache: CacheConfig {
max_size: 0,
estimated_items: 100_000,
},
shard_prefix_bits: 0,
reversed: true,
iterable: false,
hints: None,
direct_io: false,
io_backend: IoBackend::Pwrite,
#[cfg(feature = "encryption")]
encryption_key: None,
};
match self {
Workload::Balanced => common,
Workload::LowLatency => Config {
shard_count: 4,
max_file_size: 64 * 1024 * 1024,
write_buffer_size: 256 * 1024,
..common
},
Workload::HighThroughput => Config {
shard_count: 64,
max_file_size: 512 * 1024 * 1024,
write_buffer_size: 4 * 1024 * 1024,
compaction_threshold: 0.25,
..common
},
Workload::Durable => Config {
shard_count: 16,
max_file_size: 128 * 1024 * 1024,
write_buffer_size: 64 * 1024,
enable_fsync: true,
..common
},
Workload::SmallValues => Config {
direct_io: true,
#[cfg(feature = "var-collections")]
block_cache: CacheConfig {
max_size: 512 << 20,
estimated_items: 131_072,
},
#[cfg(feature = "var-collections")]
value_cache: CacheConfig {
max_size: 0,
estimated_items: 100_000,
},
..common
},
Workload::LargeValues => Config {
max_file_size: 1 << 30, write_buffer_size: 4 * 1024 * 1024,
direct_io: true,
#[cfg(feature = "var-collections")]
block_cache: CacheConfig {
max_size: 0,
estimated_items: 100_000,
},
#[cfg(feature = "var-collections")]
value_cache: CacheConfig {
max_size: 512 << 20,
estimated_items: 8192,
},
..common
},
}
}
}
#[bon::bon]
impl Config {
#[builder(builder_type = ConfigBuilder, finish_fn = build)]
pub fn for_workload(
#[builder(start_fn)] workload: Workload,
shard_count: Option<usize>,
max_file_size: Option<u64>,
write_buffer_size: Option<usize>,
compaction_threshold: Option<f64>,
compaction_max_files_per_pass: Option<usize>,
enable_fsync: Option<bool>,
direct_io: Option<bool>,
io_backend: Option<IoBackend>,
reversed: Option<bool>,
iterable: Option<bool>,
shard_prefix_bits: Option<usize>,
hints: Option<bool>,
#[cfg(feature = "var-collections")] block_cache: Option<CacheConfig>,
#[cfg(feature = "var-collections")] value_cache: Option<CacheConfig>,
#[cfg(feature = "encryption")] encryption_key: Option<[u8; 32]>,
) -> Config {
let b = workload.base();
Config {
shard_count: shard_count.unwrap_or(b.shard_count),
max_file_size: max_file_size.unwrap_or(b.max_file_size),
write_buffer_size: write_buffer_size.unwrap_or(b.write_buffer_size),
compaction_threshold: compaction_threshold.unwrap_or(b.compaction_threshold),
compaction_max_files_per_pass: compaction_max_files_per_pass
.unwrap_or(b.compaction_max_files_per_pass),
enable_fsync: enable_fsync.unwrap_or(b.enable_fsync),
direct_io: direct_io.unwrap_or(b.direct_io),
io_backend: io_backend.unwrap_or(b.io_backend),
reversed: reversed.unwrap_or(b.reversed),
iterable: iterable.unwrap_or(b.iterable),
shard_prefix_bits: shard_prefix_bits.unwrap_or(b.shard_prefix_bits),
hints,
#[cfg(feature = "var-collections")]
block_cache: block_cache.unwrap_or(b.block_cache),
#[cfg(feature = "var-collections")]
value_cache: value_cache.unwrap_or(b.value_cache),
#[cfg(feature = "encryption")]
encryption_key: encryption_key.or(b.encryption_key),
}
}
}
impl Config {
pub fn balanced() -> ConfigBuilder {
Self::for_workload(Workload::Balanced)
}
pub fn low_latency() -> ConfigBuilder {
Self::for_workload(Workload::LowLatency)
}
pub fn high_throughput() -> ConfigBuilder {
Self::for_workload(Workload::HighThroughput)
}
pub fn durable() -> ConfigBuilder {
Self::for_workload(Workload::Durable)
}
pub fn large_values() -> ConfigBuilder {
Self::for_workload(Workload::LargeValues)
}
pub fn small_values() -> ConfigBuilder {
Self::for_workload(Workload::SmallValues)
}
}
#[cfg(test)]
mod io_backend_tests {
use super::*;
#[test]
fn default_io_backend_is_pwrite() {
let cfg = Config::balanced().build();
assert_eq!(cfg.io_backend, IoBackend::Pwrite);
}
#[test]
fn io_backend_default_impl_matches_config_default() {
assert_eq!(IoBackend::default(), IoBackend::Pwrite);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_for_test() -> Config {
Config {
shard_count: 2,
max_file_size: 256 * 1024 * 1024,
compaction_threshold: 0.3,
compaction_max_files_per_pass: DEFAULT_COMPACTION_MAX_FILES_PER_PASS,
enable_fsync: false,
write_buffer_size: 1024 * 1024,
#[cfg(feature = "var-collections")]
block_cache: CacheConfig::default(),
#[cfg(feature = "var-collections")]
value_cache: CacheConfig::default(),
shard_prefix_bits: 0,
reversed: true,
iterable: false,
hints: None,
direct_io: false,
io_backend: IoBackend::default(),
#[cfg(feature = "encryption")]
encryption_key: None,
}
}
#[test]
fn test_default_config_is_valid() {
let cfg = Config::balanced().build();
assert!(cfg.shard_count >= 1);
assert!(cfg.validate().is_ok());
}
#[test]
fn validate_rejects_max_file_size_smaller_than_write_buffer() {
let mut cfg = default_for_test();
cfg.max_file_size = 4096;
cfg.write_buffer_size = 8192;
assert!(cfg.validate().is_err());
}
#[test]
fn validate_rejects_write_buffer_at_u32_limit() {
let mut cfg = default_for_test();
cfg.write_buffer_size = (u32::MAX as usize) - 4095;
cfg.max_file_size = u32::MAX as u64;
assert!(cfg.validate().is_err());
}
#[cfg(feature = "encryption")]
#[test]
fn validate_rejects_small_write_buffer_under_encryption() {
let mut cfg = default_for_test();
cfg.encryption_key = Some([0u8; 32]);
cfg.write_buffer_size = 4096;
assert!(cfg.validate().is_err());
}
#[cfg(feature = "encryption")]
#[test]
fn validate_rejects_non_page_multiple_write_buffer_under_encryption() {
let mut cfg = default_for_test();
cfg.encryption_key = Some([0u8; 32]);
cfg.write_buffer_size = 8192 + 100;
cfg.max_file_size = 256 * 1024 * 1024;
assert!(cfg.validate().is_err());
}
#[cfg(feature = "encryption")]
#[test]
fn validate_accepts_page_multiple_8k_under_encryption() {
let mut cfg = default_for_test();
cfg.encryption_key = Some([0u8; 32]);
cfg.write_buffer_size = 8192;
cfg.max_file_size = 256 * 1024 * 1024;
assert!(cfg.validate().is_ok());
}
#[test]
fn validate_accepts_plain_4096_write_buffer() {
let mut cfg = default_for_test();
cfg.write_buffer_size = 4096;
cfg.max_file_size = 8192;
assert!(cfg.validate().is_ok());
}
#[test]
fn validate_rejects_shard_prefix_bits_over_255() {
let mut cfg = default_for_test();
cfg.shard_prefix_bits = 256;
let err = cfg.validate().unwrap_err();
assert!(
err.to_string().contains("shard_prefix_bits"),
"expected shard_prefix_bits error, got: {err}"
);
}
#[test]
fn validate_rejects_compaction_threshold_nan() {
let mut cfg = default_for_test();
cfg.compaction_threshold = f64::NAN;
assert!(cfg.validate().is_err());
}
#[test]
fn validate_rejects_compaction_threshold_negative() {
let mut cfg = default_for_test();
cfg.compaction_threshold = -0.1;
assert!(cfg.validate().is_err());
}
#[test]
fn validate_rejects_compaction_threshold_over_one() {
let mut cfg = default_for_test();
cfg.compaction_threshold = 1.1;
assert!(cfg.validate().is_err());
}
#[test]
fn validate_accepts_compaction_threshold_half() {
let mut cfg = default_for_test();
cfg.compaction_threshold = 0.5;
assert!(cfg.validate().is_ok());
}
#[test]
fn validate_accepts_compaction_threshold_boundaries() {
let mut cfg = default_for_test();
cfg.compaction_threshold = 0.0;
assert!(cfg.validate().is_ok());
cfg.compaction_threshold = 1.0;
assert!(cfg.validate().is_ok());
}
#[test]
fn direct_io_requires_page_aligned_write_buffer() {
let mut c = default_for_test();
c.direct_io = true;
c.write_buffer_size = 5000; assert!(c.validate().is_err());
c.write_buffer_size = 8192;
assert!(c.validate().is_ok());
c.write_buffer_size = 4096; assert!(c.validate().is_err());
}
#[test]
fn direct_io_defaults_false() {
assert!(!Config::balanced().build().direct_io);
}
#[test]
fn with_resolved_hints_fills_none_with_default() {
let cfg = default_for_test();
assert_eq!(cfg.clone().with_resolved_hints(true).hints, Some(true));
assert_eq!(cfg.with_resolved_hints(false).hints, Some(false));
}
#[test]
fn with_resolved_hints_respects_some() {
let mut cfg = default_for_test();
cfg.hints = Some(true);
assert_eq!(cfg.with_resolved_hints(false).hints, Some(true));
}
#[cfg(feature = "var-collections")]
#[test]
fn var_cache_warn_needed_true_when_zero() {
let mut cfg = default_for_test();
cfg.block_cache.max_size = 0;
cfg.value_cache.max_size = 0;
assert!(cfg.var_cache_warn_needed());
cfg.block_cache.max_size = 1 << 20;
assert!(!cfg.var_cache_warn_needed());
}
#[cfg(feature = "var-collections")]
#[test]
fn small_values_preset_targets_block_cache() {
let c = Config::small_values().build();
assert!(c.direct_io);
assert_eq!(c.block_cache.max_size, 512 << 20);
assert_eq!(c.value_cache.max_size, 0);
}
#[cfg(feature = "var-collections")]
#[test]
fn large_values_preset_targets_value_cache() {
let c = Config::large_values().build();
assert!(c.direct_io);
assert_eq!(c.value_cache.max_size, 512 << 20);
assert_eq!(c.block_cache.max_size, 0);
}
#[cfg(feature = "var-collections")]
#[test]
fn cache_warn_only_when_both_zero() {
let mut cfg = Config::test();
cfg.block_cache.max_size = 0;
cfg.value_cache.max_size = 0;
assert!(cfg.var_cache_warn_needed());
cfg.block_cache.max_size = 1 << 20;
assert!(!cfg.var_cache_warn_needed());
cfg.block_cache.max_size = 0;
cfg.value_cache.max_size = 1 << 20;
assert!(!cfg.var_cache_warn_needed());
}
#[cfg(feature = "var-collections")]
#[test]
fn app_cache_without_direct_io_predicate() {
let mut cfg = Config::test(); cfg.block_cache.max_size = 0;
cfg.value_cache.max_size = 0;
assert!(!cfg.app_cache_without_direct_io(), "no cache -> no warning");
cfg.block_cache.max_size = 1 << 20;
assert!(
cfg.app_cache_without_direct_io(),
"cache on + buffered -> warn"
);
cfg.direct_io = true;
assert!(
!cfg.app_cache_without_direct_io(),
"direct_io on -> no warning"
);
assert!(!Config::small_values().build().app_cache_without_direct_io());
assert!(!Config::large_values().build().app_cache_without_direct_io());
}
#[test]
fn workload_balanced_matches_table() {
let c = Config::balanced().build();
assert_eq!(c.max_file_size, 256 * 1024 * 1024);
assert_eq!(c.write_buffer_size, 1024 * 1024);
assert_eq!(c.compaction_threshold, 0.30);
assert!(!c.enable_fsync);
assert!(!c.direct_io);
assert_eq!(c.hints, None);
assert!(c.shard_count >= 1);
c.validate().unwrap();
}
#[test]
fn workload_high_throughput_matches_table() {
let c = Config::high_throughput().build();
assert_eq!(c.shard_count, 64);
assert_eq!(c.max_file_size, 512 * 1024 * 1024);
assert_eq!(c.write_buffer_size, 4 * 1024 * 1024);
assert_eq!(c.compaction_threshold, 0.25);
c.validate().unwrap();
}
#[test]
fn workload_durable_keeps_fsync_placeholder() {
let c = Config::durable().build();
assert_eq!(c.shard_count, 16);
assert_eq!(c.write_buffer_size, 64 * 1024);
assert!(c.enable_fsync);
c.validate().unwrap();
}
#[cfg(feature = "var-collections")]
#[test]
fn workload_large_values_has_cache_and_direct_io() {
let c = Config::large_values().build();
assert_eq!(c.max_file_size, 1 << 30);
assert_eq!(c.write_buffer_size, 4 * 1024 * 1024);
assert!(c.direct_io);
assert_eq!(c.value_cache.max_size, 512 << 20);
assert_eq!(c.value_cache.estimated_items, 8192);
assert_eq!(c.block_cache.max_size, 0);
c.validate().unwrap();
}
#[test]
fn compaction_max_files_per_pass_default_and_override() {
assert_eq!(Config::balanced().build().compaction_max_files_per_pass, 4);
assert_eq!(Config::test().compaction_max_files_per_pass, 4);
let c = Config::for_workload(Workload::Balanced)
.compaction_max_files_per_pass(0)
.build();
assert_eq!(c.compaction_max_files_per_pass, 0);
}
#[test]
fn for_workload_overrides_apply() {
let c = Config::for_workload(Workload::Balanced)
.shard_count(7)
.compaction_threshold(0.5)
.hints(true)
.build();
assert_eq!(c.shard_count, 7);
assert_eq!(c.compaction_threshold, 0.5);
assert_eq!(c.hints, Some(true));
assert_eq!(c.max_file_size, 256 * 1024 * 1024);
}
#[cfg(feature = "encryption")]
#[test]
fn large_values_with_encryption_validates() {
let c = Config::large_values().encryption_key([0u8; 32]).build();
c.validate().unwrap();
}
#[test]
fn iterable_defaults_false_and_overridable() {
assert!(!Config::balanced().build().iterable);
assert!(Config::balanced().iterable(true).build().iterable);
assert!(!Config::test().iterable); }
}