use std::sync::Arc;
use crate::engine::internal_key::INTERNAL_KEY_SUFFIX_LEN;
pub const DEFAULT_MAX_KEY_SIZE: usize = 8 * 1024 * 1024;
pub const DEFAULT_TRANSACTION_KEYS_INLINE: usize = 32;
pub const DEFAULT_MAX_VALUE_SIZE: usize = 64 * 1024 * 1024;
pub const MAX_BLOCK_CACHE_SHARD_BITS: u32 = 8;
pub const MAX_BLOOM_BITS_PER_KEY: usize = 64;
#[cfg(target_family = "wasm")]
pub const DEFAULT_MAX_BACKGROUND_COMPACTIONS: usize = 0;
#[cfg(not(target_family = "wasm"))]
pub const DEFAULT_MAX_BACKGROUND_COMPACTIONS: usize = 1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CompactionDecision {
Keep,
Remove,
Change(Vec<u8>),
}
pub trait CompactionFilter: Send + Sync + 'static {
fn filter(&self, level: usize, key: &[u8], value: &[u8]) -> CompactionDecision;
fn filter_range_delete(&self, level: usize, start: &[u8], end: &[u8]) -> CompactionDecision {
let _ = (level, start, end);
CompactionDecision::Keep
}
fn name(&self) -> &'static str;
}
pub trait MergeOperator: Send + Sync + 'static {
fn full_merge(&self, key: &[u8], base: Option<&[u8]>, operands: &[&[u8]]) -> Option<Vec<u8>>;
fn partial_merge(&self, key: &[u8], left: &[u8], right: &[u8]) -> Option<Vec<u8>> {
let _ = (key, left, right);
None
}
fn name(&self) -> &'static str;
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct WriteOptions {
pub sync: bool,
pub disable_wal: bool,
pub low_pri: bool,
pub no_slowdown: bool,
}
impl WriteOptions {
pub fn new() -> Self {
Self::default()
}
pub fn sync() -> Self {
Self {
sync: true,
..Self::default()
}
}
pub fn disable_wal() -> Self {
Self {
disable_wal: true,
..Self::default()
}
}
}
pub trait PrefixExtractor: Send + Sync + 'static {
fn extract<'a>(&self, key: &'a [u8]) -> Option<&'a [u8]>;
fn extract_query<'a>(&self, prefix: &'a [u8]) -> Option<&'a [u8]> {
let extracted = self.extract(prefix)?;
(extracted == prefix).then_some(extracted)
}
fn name(&self) -> &'static str;
}
#[derive(Debug, Clone, Copy)]
pub struct FixedLengthPrefix(pub usize);
impl PrefixExtractor for FixedLengthPrefix {
fn extract<'a>(&self, key: &'a [u8]) -> Option<&'a [u8]> {
if key.len() >= self.0 {
Some(&key[..self.0])
} else {
None
}
}
fn extract_query<'a>(&self, prefix: &'a [u8]) -> Option<&'a [u8]> {
self.extract(prefix)
}
fn name(&self) -> &'static str {
"FixedLengthPrefix"
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum CompactionStyle {
#[default]
Level,
Fifo,
Universal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FifoCompactionOptions {
pub max_table_files_size: u64,
}
impl Default for FifoCompactionOptions {
fn default() -> Self {
Self {
max_table_files_size: 1024 * 1024 * 1024,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UniversalCompactionOptions {
pub size_ratio: u32,
pub min_merge_width: u32,
pub max_merge_width: u32,
pub max_size_amplification_percent: u32,
}
impl Default for UniversalCompactionOptions {
fn default() -> Self {
Self {
size_ratio: 1,
min_merge_width: 2,
max_merge_width: u32::MAX,
max_size_amplification_percent: 200,
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum DurabilityMode {
Immediate,
#[default]
Eventual,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum CompressionType {
None,
Snappy,
#[default]
Lz4,
}
pub use crate::engine::arena::ArenaProfile;
#[derive(Clone)]
pub struct Options {
pub write_buffer_size: usize,
pub arena_profile: ArenaProfile,
pub block_size: usize,
pub block_cache_size: usize,
pub block_cache_num_shard_bits: u32,
pub strict_capacity_limit: bool,
pub bloom_bits_per_key: usize,
pub compression: CompressionType,
pub compression_per_level: Option<Vec<CompressionType>>,
pub l0_compaction_trigger: usize,
pub level_base_bytes: u64,
pub level_size_multiplier: u64,
pub target_file_size: u64,
pub durability: DurabilityMode,
pub compaction_filter: Option<Arc<dyn CompactionFilter>>,
pub prefix_extractor: Option<Arc<dyn PrefixExtractor>>,
pub merge_operator: Option<Arc<dyn MergeOperator>>,
pub atomic_flush: bool,
pub listeners: Vec<Arc<dyn crate::EventListener>>,
pub statistics: Option<Arc<crate::Statistics>>,
pub rate_limiter: Option<Arc<dyn crate::RateLimiter>>,
pub level0_slowdown_writes_trigger: usize,
pub level0_stop_writes_trigger: usize,
pub soft_pending_compaction_bytes_limit: u64,
pub hard_pending_compaction_bytes_limit: u64,
pub max_write_buffer_number: usize,
pub compaction_style: CompactionStyle,
pub fifo_compaction_options: FifoCompactionOptions,
pub universal_compaction_options: UniversalCompactionOptions,
pub max_background_compactions: usize,
pub max_subcompactions: usize,
pub evict_compaction_data_from_page_cache: bool,
pub partitioned_index: bool,
pub cache_index_and_filter_blocks: bool,
pub metadata_block_size: usize,
pub read_only: bool,
pub max_key_size: usize,
pub max_value_size: usize,
pub transaction_keys_inline: usize,
pub env: Arc<dyn crate::env::Env>,
}
impl Default for Options {
fn default() -> Self {
Self {
write_buffer_size: 64 * 1024 * 1024,
arena_profile: ArenaProfile::SERVER,
block_size: 16 * 1024,
block_cache_size: 512 * 1024 * 1024,
block_cache_num_shard_bits: 6,
strict_capacity_limit: false,
bloom_bits_per_key: 10,
compression: CompressionType::Lz4,
compression_per_level: None,
l0_compaction_trigger: 4,
level_base_bytes: 256 * 1024 * 1024,
level_size_multiplier: 10,
target_file_size: 64 * 1024 * 1024,
durability: DurabilityMode::Eventual,
compaction_filter: None,
prefix_extractor: None,
merge_operator: None,
atomic_flush: false,
listeners: Vec::new(),
statistics: None,
rate_limiter: None,
level0_slowdown_writes_trigger: 20,
level0_stop_writes_trigger: 36,
soft_pending_compaction_bytes_limit: 64 * 1024 * 1024 * 1024,
hard_pending_compaction_bytes_limit: 256 * 1024 * 1024 * 1024,
max_write_buffer_number: 2,
compaction_style: CompactionStyle::Level,
fifo_compaction_options: FifoCompactionOptions::default(),
universal_compaction_options: UniversalCompactionOptions::default(),
evict_compaction_data_from_page_cache: false,
max_background_compactions: DEFAULT_MAX_BACKGROUND_COMPACTIONS,
max_subcompactions: 1,
partitioned_index: false,
cache_index_and_filter_blocks: false,
metadata_block_size: 4096,
read_only: false,
max_key_size: DEFAULT_MAX_KEY_SIZE,
max_value_size: DEFAULT_MAX_VALUE_SIZE,
transaction_keys_inline: DEFAULT_TRANSACTION_KEYS_INLINE,
env: crate::env::std_env(),
}
}
}
impl std::fmt::Debug for Options {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Options")
.field("write_buffer_size", &self.write_buffer_size)
.field("arena_profile", &self.arena_profile)
.field("block_size", &self.block_size)
.field("block_cache_size", &self.block_cache_size)
.field(
"block_cache_num_shard_bits",
&self.block_cache_num_shard_bits,
)
.field("strict_capacity_limit", &self.strict_capacity_limit)
.field("bloom_bits_per_key", &self.bloom_bits_per_key)
.field("compression", &self.compression)
.field("compression_per_level", &self.compression_per_level)
.field("l0_compaction_trigger", &self.l0_compaction_trigger)
.field("level_base_bytes", &self.level_base_bytes)
.field("level_size_multiplier", &self.level_size_multiplier)
.field("target_file_size", &self.target_file_size)
.field("durability", &self.durability)
.field(
"compaction_filter",
&self.compaction_filter.as_ref().map(|f| f.name()),
)
.field(
"prefix_extractor",
&self.prefix_extractor.as_ref().map(|p| p.name()),
)
.field(
"merge_operator",
&self.merge_operator.as_ref().map(|m| m.name()),
)
.field("atomic_flush", &self.atomic_flush)
.field("listeners", &self.listeners.len())
.field("statistics", &self.statistics.is_some())
.field("rate_limiter", &self.rate_limiter.is_some())
.field(
"level0_slowdown_writes_trigger",
&self.level0_slowdown_writes_trigger,
)
.field(
"level0_stop_writes_trigger",
&self.level0_stop_writes_trigger,
)
.field(
"soft_pending_compaction_bytes_limit",
&self.soft_pending_compaction_bytes_limit,
)
.field(
"hard_pending_compaction_bytes_limit",
&self.hard_pending_compaction_bytes_limit,
)
.field("max_write_buffer_number", &self.max_write_buffer_number)
.field("compaction_style", &self.compaction_style)
.field("fifo_compaction_options", &self.fifo_compaction_options)
.field(
"universal_compaction_options",
&self.universal_compaction_options,
)
.field(
"evict_compaction_data_from_page_cache",
&self.evict_compaction_data_from_page_cache,
)
.field(
"max_background_compactions",
&self.max_background_compactions,
)
.field("max_subcompactions", &self.max_subcompactions)
.field("partitioned_index", &self.partitioned_index)
.field(
"cache_index_and_filter_blocks",
&self.cache_index_and_filter_blocks,
)
.field("metadata_block_size", &self.metadata_block_size)
.field("read_only", &self.read_only)
.field("max_key_size", &self.max_key_size)
.field("max_value_size", &self.max_value_size)
.field("env", &self.env)
.finish()
}
}
impl Options {
pub fn embedded() -> Self {
Self {
write_buffer_size: 256 * 1024,
arena_profile: ArenaProfile::EMBEDDED,
block_size: 4 * 1024,
block_cache_size: 0,
block_cache_num_shard_bits: 0,
l0_compaction_trigger: 2,
level_base_bytes: 1024 * 1024,
target_file_size: 256 * 1024,
level0_slowdown_writes_trigger: 4,
level0_stop_writes_trigger: 8,
soft_pending_compaction_bytes_limit: 4 * 1024 * 1024,
hard_pending_compaction_bytes_limit: 16 * 1024 * 1024,
max_write_buffer_number: 2,
max_background_compactions: 0,
evict_compaction_data_from_page_cache: true,
metadata_block_size: 1024,
max_key_size: 16 * 1024,
max_value_size: 256 * 1024,
transaction_keys_inline: 16,
..Self::default()
}
}
pub fn wasm() -> Self {
Self {
write_buffer_size: 1024 * 1024,
arena_profile: ArenaProfile::EMBEDDED,
block_size: 4 * 1024,
block_cache_size: 1024 * 1024,
block_cache_num_shard_bits: 0,
l0_compaction_trigger: 4,
level_base_bytes: 8 * 1024 * 1024,
target_file_size: 1024 * 1024,
level0_slowdown_writes_trigger: 8,
level0_stop_writes_trigger: 12,
soft_pending_compaction_bytes_limit: 32 * 1024 * 1024,
hard_pending_compaction_bytes_limit: 128 * 1024 * 1024,
max_write_buffer_number: 2,
max_background_compactions: 0,
evict_compaction_data_from_page_cache: false,
metadata_block_size: 1024,
max_key_size: 16 * 1024,
max_value_size: 1024 * 1024,
transaction_keys_inline: 16,
..Self::default()
}
}
pub fn memory() -> Self {
Self {
env: Arc::new(crate::env::MemEnv::new()),
max_background_compactions: 0,
durability: DurabilityMode::Eventual,
..Self::embedded()
}
}
pub fn validate(&self) -> crate::Result<()> {
require_nonzero_usize("write_buffer_size", self.write_buffer_size)?;
require_nonzero_usize("block_size", self.block_size)?;
require_nonzero_usize("l0_compaction_trigger", self.l0_compaction_trigger)?;
require_nonzero_u64("level_base_bytes", self.level_base_bytes)?;
require_nonzero_u64("level_size_multiplier", self.level_size_multiplier)?;
require_nonzero_u64("target_file_size", self.target_file_size)?;
require_nonzero_usize("metadata_block_size", self.metadata_block_size)?;
if self.max_background_compactions > 0 && !self.env.capabilities().threads {
return invalid_option(
"max_background_compactions",
"must be 0 for an environment that cannot spawn threads, such as MemEnv \
or wasm; compaction then runs on the calling thread. Options::memory() \
and Options::wasm() set this along with the rest of their profile",
);
}
if !self.arena_profile.is_valid() {
return invalid_option(
"arena_profile",
"initial_chunk_size and max_chunk_size must be powers of two with initial <= max",
);
}
const MAX_ENCODABLE_KEY: usize = u32::MAX as usize - INTERNAL_KEY_SUFFIX_LEN;
if self.max_key_size > MAX_ENCODABLE_KEY {
return invalid_option(
"max_key_size",
"must be <= u32::MAX minus the 9-byte internal-key suffix",
);
}
if self.max_value_size > u32::MAX as usize {
return invalid_option("max_value_size", "must be <= u32::MAX");
}
if self.block_cache_num_shard_bits > MAX_BLOCK_CACHE_SHARD_BITS {
return invalid_option(
"block_cache_num_shard_bits",
format!("must be <= {MAX_BLOCK_CACHE_SHARD_BITS}"),
);
}
if !(1..=MAX_BLOOM_BITS_PER_KEY).contains(&self.bloom_bits_per_key) {
return invalid_option(
"bloom_bits_per_key",
format!("must be in 1..={MAX_BLOOM_BITS_PER_KEY}"),
);
}
if self.level0_slowdown_writes_trigger > 0
&& self.level0_stop_writes_trigger > 0
&& self.level0_slowdown_writes_trigger > self.level0_stop_writes_trigger
{
return invalid_option(
"level0_slowdown_writes_trigger",
"must be <= level0_stop_writes_trigger when both triggers are nonzero",
);
}
if self.soft_pending_compaction_bytes_limit > 0
&& self.hard_pending_compaction_bytes_limit > 0
&& self.soft_pending_compaction_bytes_limit > self.hard_pending_compaction_bytes_limit
{
return invalid_option(
"soft_pending_compaction_bytes_limit",
"must be <= hard_pending_compaction_bytes_limit when both limits are nonzero",
);
}
if matches!(self.durability, DurabilityMode::Immediate)
&& !self.env.capabilities().durable_sync
{
return invalid_option(
"durability",
"DurabilityMode::Immediate needs an Env whose \
Capabilities::durable_sync is true; this one reports false, \
so a synced write would not be durable",
);
}
match self.compaction_style {
CompactionStyle::Level => {}
CompactionStyle::Fifo => {
require_nonzero_u64(
"fifo_compaction_options.max_table_files_size",
self.fifo_compaction_options.max_table_files_size,
)?;
}
CompactionStyle::Universal => {
let universal = self.universal_compaction_options;
if universal.size_ratio == 0 {
return invalid_option(
"universal_compaction_options.size_ratio",
"must be greater than 0",
);
}
if universal.min_merge_width < 2 {
return invalid_option(
"universal_compaction_options.min_merge_width",
"must be at least 2",
);
}
if universal.max_merge_width < universal.min_merge_width {
return invalid_option(
"universal_compaction_options.max_merge_width",
"must be >= universal_compaction_options.min_merge_width",
);
}
if universal.max_size_amplification_percent == 0 {
return invalid_option(
"universal_compaction_options.max_size_amplification_percent",
"must be greater than 0",
);
}
}
}
Ok(())
}
pub(crate) fn to_engine_options(&self) -> crate::engine::EngineOptions {
crate::engine::EngineOptions {
write_buffer_size: self.write_buffer_size,
arena_profile: self.arena_profile,
block_size: self.block_size,
block_cache_size: self.block_cache_size,
block_cache_num_shard_bits: self.block_cache_num_shard_bits,
strict_capacity_limit: self.strict_capacity_limit,
bloom_bits_per_key: self.bloom_bits_per_key,
compression: self.compression,
compression_per_level: self.compression_per_level.clone(),
l0_compaction_trigger: self.l0_compaction_trigger,
level_base_bytes: self.level_base_bytes,
level_size_multiplier: self.level_size_multiplier,
target_file_size: self.target_file_size,
compaction_filter: self.compaction_filter.clone(),
prefix_extractor: self.prefix_extractor.clone(),
merge_operator: self.merge_operator.clone(),
listeners: self.listeners.clone(),
statistics: self.statistics.clone(),
rate_limiter: self.rate_limiter.clone(),
level0_slowdown_writes_trigger: self.level0_slowdown_writes_trigger,
level0_stop_writes_trigger: self.level0_stop_writes_trigger,
soft_pending_compaction_bytes_limit: self.soft_pending_compaction_bytes_limit,
hard_pending_compaction_bytes_limit: self.hard_pending_compaction_bytes_limit,
max_write_buffer_number: self.max_write_buffer_number,
compaction_style: self.compaction_style,
fifo_compaction_options: self.fifo_compaction_options,
universal_compaction_options: self.universal_compaction_options,
evict_compaction_data_from_page_cache: self.evict_compaction_data_from_page_cache,
max_background_compactions: self.max_background_compactions,
partitioned_index: self.partitioned_index,
cache_index_and_filter_blocks: self.cache_index_and_filter_blocks,
metadata_block_size: self.metadata_block_size,
read_only: self.read_only,
max_key_size: self.max_key_size,
max_value_size: self.max_value_size,
env: Arc::clone(&self.env),
}
}
}
fn require_nonzero_usize(name: &'static str, value: usize) -> crate::Result<()> {
if value == 0 {
invalid_option(name, "must be greater than 0")
} else {
Ok(())
}
}
fn require_nonzero_u64(name: &'static str, value: u64) -> crate::Result<()> {
if value == 0 {
invalid_option(name, "must be greater than 0")
} else {
Ok(())
}
}
fn invalid_option(name: &'static str, requirement: impl Into<String>) -> crate::Result<()> {
Err(crate::Error::invalid_argument(format!(
"invalid option `{name}`: {}",
requirement.into()
)))
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_invalid_option(opts: Options, expected: &str) {
match opts.validate().unwrap_err() {
crate::Error::InvalidArgument(message) => {
assert!(
message.contains(expected),
"expected invalid option message to contain {expected:?}, got {message:?}"
);
}
other => panic!("expected invalid argument, got {other:?}"),
}
}
#[test]
fn fixed_length_prefix_extract() {
let ex = FixedLengthPrefix(4);
assert_eq!(ex.extract(b"tenant_001"), Some(&b"tena"[..]));
assert_eq!(ex.extract(b"abcd"), Some(&b"abcd"[..]));
assert_eq!(ex.extract(b"abc"), None);
assert_eq!(ex.extract(b""), None);
assert_eq!(ex.name(), "FixedLengthPrefix");
}
#[test]
fn fixed_length_prefix_zero_accepts_any_key() {
let ex = FixedLengthPrefix(0);
assert_eq!(ex.extract(b"anything"), Some(&b""[..]));
assert_eq!(ex.extract(b""), Some(&b""[..]));
}
#[test]
fn compaction_decision_equality_and_clone() {
assert_eq!(CompactionDecision::Keep, CompactionDecision::Keep);
assert_ne!(CompactionDecision::Keep, CompactionDecision::Remove);
let c = CompactionDecision::Change(b"new".to_vec());
assert_eq!(c.clone(), c);
assert_ne!(c, CompactionDecision::Change(b"other".to_vec()));
}
#[test]
fn write_options_defaults_are_all_false() {
let wo = WriteOptions::new();
assert!(!wo.sync);
assert!(!wo.disable_wal);
assert!(!wo.low_pri);
assert!(!wo.no_slowdown);
assert_eq!(wo, WriteOptions::default());
}
#[test]
fn write_options_sync_constructor_sets_only_sync() {
let wo = WriteOptions::sync();
assert!(wo.sync);
assert!(!wo.disable_wal);
}
#[test]
fn write_options_disable_wal_constructor_sets_only_disable_wal() {
let wo = WriteOptions::disable_wal();
assert!(wo.disable_wal);
assert!(!wo.sync);
}
#[test]
fn compaction_style_default_is_level() {
assert_eq!(CompactionStyle::default(), CompactionStyle::Level);
}
#[test]
fn fifo_compaction_options_default_is_one_gib() {
let f = FifoCompactionOptions::default();
assert_eq!(f.max_table_files_size, 1024 * 1024 * 1024);
}
#[test]
fn options_validate_accepts_defaults_and_disabled_stall_triggers() {
Options::default().validate().unwrap();
let opts = Options {
level0_slowdown_writes_trigger: 0,
level0_stop_writes_trigger: 0,
soft_pending_compaction_bytes_limit: 0,
hard_pending_compaction_bytes_limit: 0,
max_write_buffer_number: 0,
..Options::default()
};
opts.validate().unwrap();
}
#[test]
fn options_validate_accepts_zero_block_cache_size() {
Options {
block_cache_size: 0,
..Options::default()
}
.validate()
.unwrap();
}
#[test]
fn zero_block_cache_db_opens_and_reads_correctly() {
let dir = tempfile::TempDir::new().unwrap();
let db = crate::Db::open(
dir.path(),
Options {
block_cache_size: 0,
write_buffer_size: 4 * 1024,
..Options::default()
},
)
.unwrap();
for i in 0..500u32 {
db.put(
format!("key{i:04}").as_bytes(),
format!("value{i}").as_bytes(),
)
.unwrap();
}
for i in 0..500u32 {
assert_eq!(
db.get(format!("key{i:04}").as_bytes()).unwrap(),
Some(format!("value{i}").into_bytes())
);
}
assert!(db.get(b"absent").unwrap().is_none());
}
#[test]
fn options_validate_rejects_zero_core_sizes() {
assert_invalid_option(
Options {
write_buffer_size: 0,
..Options::default()
},
"write_buffer_size",
);
assert_invalid_option(
Options {
block_size: 0,
..Options::default()
},
"block_size",
);
assert_invalid_option(
Options {
l0_compaction_trigger: 0,
..Options::default()
},
"l0_compaction_trigger",
);
assert_invalid_option(
Options {
level_base_bytes: 0,
..Options::default()
},
"level_base_bytes",
);
assert_invalid_option(
Options {
level_size_multiplier: 0,
..Options::default()
},
"level_size_multiplier",
);
assert_invalid_option(
Options {
target_file_size: 0,
..Options::default()
},
"target_file_size",
);
assert_invalid_option(
Options {
metadata_block_size: 0,
..Options::default()
},
"metadata_block_size",
);
}
#[test]
fn options_validate_rejects_unsupported_ranges() {
assert_invalid_option(
Options {
block_cache_num_shard_bits: MAX_BLOCK_CACHE_SHARD_BITS + 1,
..Options::default()
},
"block_cache_num_shard_bits",
);
assert_invalid_option(
Options {
bloom_bits_per_key: 0,
..Options::default()
},
"bloom_bits_per_key",
);
assert_invalid_option(
Options {
bloom_bits_per_key: MAX_BLOOM_BITS_PER_KEY + 1,
..Options::default()
},
"bloom_bits_per_key",
);
}
#[test]
fn zero_background_compactions_is_a_supported_configuration() {
Options {
max_background_compactions: 0,
..Options::default()
}
.validate()
.expect("zero background workers selects foreground compaction, not an error");
}
#[test]
fn embedded_profile_validates() {
Options::embedded()
.validate()
.expect("the shipped embedded profile must satisfy its own invariants");
}
#[test]
fn embedded_profile_bounds_every_resident_term() {
let o = Options::embedded();
assert_eq!(o.write_buffer_size, 256 * 1024);
assert_eq!(o.max_write_buffer_number, 2);
assert_eq!(o.block_cache_size, 0);
assert_eq!(o.block_cache_num_shard_bits, 0);
assert_eq!(o.block_size, 4 * 1024);
assert_eq!(o.target_file_size, 256 * 1024);
assert_eq!(o.metadata_block_size, 1024);
assert!(o.max_value_size <= o.write_buffer_size);
assert!(o.max_key_size < o.write_buffer_size);
assert_eq!(o.max_background_compactions, 0);
}
#[test]
fn wasm_profile_validates() {
Options::wasm()
.validate()
.expect("the shipped wasm profile must satisfy its own invariants");
}
#[test]
fn wasm_profile_bounds_every_resident_term() {
let o = Options::wasm();
assert_eq!(o.write_buffer_size, 1024 * 1024);
assert_eq!(o.max_write_buffer_number, 2);
assert_eq!(o.arena_profile.max_chunk_size, 64 * 1024);
assert_eq!(o.block_cache_size, 1024 * 1024);
assert_eq!(o.block_cache_num_shard_bits, 0);
assert!(o.max_value_size <= o.write_buffer_size);
assert!(o.max_key_size < o.write_buffer_size);
assert_eq!(o.max_background_compactions, 0);
assert!(!o.evict_compaction_data_from_page_cache);
assert_eq!(o.metadata_block_size, 1024);
}
#[test]
fn wasm_profile_is_smaller_than_the_default_but_roomier_than_embedded() {
let d = Options::default();
let w = Options::wasm();
let e = Options::embedded();
assert!(w.write_buffer_size < d.write_buffer_size);
assert!(w.block_cache_size < d.block_cache_size);
assert!(w.max_value_size < d.max_value_size);
assert!(w.hard_pending_compaction_bytes_limit < d.hard_pending_compaction_bytes_limit);
assert!(w.write_buffer_size > e.write_buffer_size);
assert!(w.target_file_size > e.target_file_size);
assert!(w.level_base_bytes > e.level_base_bytes);
assert!(w.max_value_size > e.max_value_size);
assert_eq!(e.block_cache_size, 0);
assert!(w.block_cache_size > e.block_cache_size);
}
#[test]
fn default_profile_opens_on_every_target_it_is_compiled_for() {
Options::default()
.validate()
.expect("Options::default must be valid on the target it was built for");
#[cfg(target_family = "wasm")]
assert_eq!(Options::default().max_background_compactions, 0);
}
#[test]
fn embedded_profile_is_strictly_smaller_than_the_default() {
let d = Options::default();
let e = Options::embedded();
assert!(e.write_buffer_size < d.write_buffer_size);
assert!(e.block_cache_size < d.block_cache_size);
assert!(e.block_size < d.block_size);
assert!(e.target_file_size < d.target_file_size);
assert!(e.level_base_bytes < d.level_base_bytes);
assert!(e.max_key_size < d.max_key_size);
assert!(e.max_value_size < d.max_value_size);
assert!(e.level0_stop_writes_trigger < d.level0_stop_writes_trigger);
assert!(e.hard_pending_compaction_bytes_limit < d.hard_pending_compaction_bytes_limit);
}
#[test]
fn options_validate_rejects_inconsistent_stall_thresholds() {
assert_invalid_option(
Options {
level0_slowdown_writes_trigger: 10,
level0_stop_writes_trigger: 5,
..Options::default()
},
"level0_slowdown_writes_trigger",
);
assert_invalid_option(
Options {
soft_pending_compaction_bytes_limit: 10,
hard_pending_compaction_bytes_limit: 5,
..Options::default()
},
"soft_pending_compaction_bytes_limit",
);
}
#[test]
fn options_validate_checks_selected_compaction_style_options() {
Options {
fifo_compaction_options: FifoCompactionOptions {
max_table_files_size: 0,
},
..Options::default()
}
.validate()
.unwrap();
assert_invalid_option(
Options {
compaction_style: CompactionStyle::Fifo,
fifo_compaction_options: FifoCompactionOptions {
max_table_files_size: 0,
},
..Options::default()
},
"fifo_compaction_options.max_table_files_size",
);
assert_invalid_option(
Options {
compaction_style: CompactionStyle::Universal,
universal_compaction_options: UniversalCompactionOptions {
size_ratio: 0,
..UniversalCompactionOptions::default()
},
..Options::default()
},
"universal_compaction_options.size_ratio",
);
assert_invalid_option(
Options {
compaction_style: CompactionStyle::Universal,
universal_compaction_options: UniversalCompactionOptions {
min_merge_width: 1,
..UniversalCompactionOptions::default()
},
..Options::default()
},
"universal_compaction_options.min_merge_width",
);
assert_invalid_option(
Options {
compaction_style: CompactionStyle::Universal,
universal_compaction_options: UniversalCompactionOptions {
min_merge_width: 4,
max_merge_width: 3,
..UniversalCompactionOptions::default()
},
..Options::default()
},
"universal_compaction_options.max_merge_width",
);
assert_invalid_option(
Options {
compaction_style: CompactionStyle::Universal,
universal_compaction_options: UniversalCompactionOptions {
max_size_amplification_percent: 0,
..UniversalCompactionOptions::default()
},
..Options::default()
},
"universal_compaction_options.max_size_amplification_percent",
);
}
#[test]
fn embedded_preset_is_valid_and_small() {
let opts = Options::embedded();
opts.validate()
.expect("the preset must be a valid option set");
assert_eq!(opts.arena_profile, ArenaProfile::EMBEDDED);
assert_eq!(
opts.to_engine_options().arena_profile,
ArenaProfile::EMBEDDED
);
let w = opts.write_buffer_size;
let c = opts.arena_profile.max_chunk_size;
let m = opts.max_write_buffer_number;
assert_eq!(2 * m * (w + c) + m * w, 1792 * 1024);
}
#[test]
fn arena_profile_validation_rejects_bad_shapes() {
let bad = [
ArenaProfile {
initial_chunk_size: 3000,
max_chunk_size: 64 * 1024,
},
ArenaProfile {
initial_chunk_size: 4096,
max_chunk_size: 3000,
},
ArenaProfile {
initial_chunk_size: 128 * 1024,
max_chunk_size: 64 * 1024,
},
ArenaProfile {
initial_chunk_size: 0,
max_chunk_size: 64 * 1024,
},
];
for profile in bad {
assert!(!profile.is_valid(), "{profile:?} must be rejected");
let opts = Options {
arena_profile: profile,
..Options::default()
};
match opts.validate() {
Err(crate::Error::InvalidArgument(message)) => {
assert!(message.contains("arena_profile"), "{message}")
}
other => panic!("expected an arena_profile error, got {other:?}"),
}
}
assert!(ArenaProfile::SERVER.is_valid());
assert!(ArenaProfile::EMBEDDED.is_valid());
assert_eq!(ArenaProfile::default(), ArenaProfile::SERVER);
}
#[test]
fn key_and_value_size_caps_are_encodable() {
for (name, opts) in [
(
"max_key_size",
Options {
max_key_size: u32::MAX as usize + 1,
..Options::default()
},
),
(
"max_value_size",
Options {
max_value_size: u32::MAX as usize + 1,
..Options::default()
},
),
] {
match opts.validate() {
Err(crate::Error::InvalidArgument(message)) => {
assert!(message.contains(name), "{message}")
}
other => panic!("expected a {name} error, got {other:?}"),
}
}
}
#[test]
fn options_to_engine_options_preserves_engine_fields() {
let opts = Options::default();
let eo = opts.to_engine_options();
assert_eq!(eo.write_buffer_size, opts.write_buffer_size);
assert_eq!(eo.block_size, opts.block_size);
assert_eq!(eo.block_cache_size, opts.block_cache_size);
assert_eq!(eo.bloom_bits_per_key, opts.bloom_bits_per_key);
assert_eq!(eo.l0_compaction_trigger, opts.l0_compaction_trigger);
assert_eq!(eo.level_base_bytes, opts.level_base_bytes);
assert_eq!(eo.level_size_multiplier, opts.level_size_multiplier);
assert_eq!(eo.target_file_size, opts.target_file_size);
assert_eq!(eo.compaction_style, opts.compaction_style);
assert_eq!(eo.read_only, opts.read_only);
assert_eq!(eo.max_key_size, opts.max_key_size);
assert_eq!(eo.max_value_size, opts.max_value_size);
assert_eq!(
eo.max_background_compactions,
opts.max_background_compactions
);
assert_eq!(
eo.evict_compaction_data_from_page_cache,
opts.evict_compaction_data_from_page_cache
);
assert_eq!(eo.partitioned_index, opts.partitioned_index);
assert_eq!(
eo.cache_index_and_filter_blocks,
opts.cache_index_and_filter_blocks
);
}
#[test]
fn custom_prefix_extractor_can_be_plugged_in() {
struct UntilColon;
impl PrefixExtractor for UntilColon {
fn extract<'a>(&self, key: &'a [u8]) -> Option<&'a [u8]> {
key.iter().position(|&b| b == b':').map(|i| &key[..i])
}
fn name(&self) -> &'static str {
"UntilColon"
}
}
let ex: Arc<dyn PrefixExtractor> = Arc::new(UntilColon);
assert_eq!(ex.extract(b"tenant:key"), Some(&b"tenant"[..]));
assert_eq!(ex.extract(b"nocolon"), None);
assert_eq!(ex.name(), "UntilColon");
}
}