use std::{mem::size_of, path::PathBuf};
use itoa::Buffer;
use wdev::{detect_cpu_cores, detect_system_memory};
use whlog::{
DEFAULT_MUTABLE_FRACTION, DEFAULT_NUM_PAGES, DEFAULT_PAGE_SIZE, HybridLogConfig, SECTOR_ALIGNMENT,
};
use windex::HashBucket;
use zmij::Buffer as FmtBuffer;
use crate::error::{Error, Result};
pub const DEFAULT_INDEX_SIZE: usize = 65536;
pub const INDEX_BUCKET_BYTES: usize = size_of::<HashBucket>();
pub const INDEX_BUCKET_DATA_SLOTS: usize = HashBucket::DATA_ENTRIES;
pub const DEFAULT_MAX_SESSIONS: usize = 128;
pub const MIN_MEMORY_BUDGET_BYTES: u64 = 256 * 1024 * 1024;
pub const MAX_DEFAULT_MEMORY_BUDGET_BYTES: u64 = 32 * 1024 * 1024 * 1024;
pub const DEFAULT_MEMORY_PERCENT: u64 = 25;
pub const SESSIONS_PER_CORE: usize = 16;
pub const MIN_SESSIONS: usize = 128;
pub const MAX_SESSIONS: usize = 1024;
pub const MIN_INDEX_SIZE: usize = 65536;
pub const MAX_INDEX_SIZE: usize = 16_777_216;
pub const MIN_NUM_PAGES: usize = 16;
pub const MAX_NUM_PAGES: usize = 1_048_576;
pub const DEFAULT_READ_CACHE_NUM_PAGES: usize = 64;
pub const DEFAULT_GC_SCAN_INTERVAL_MS: u64 = 5_000;
pub const DEFAULT_GC_COMPACTION_INTERVAL_MS: u64 = 60_000;
pub const DEFAULT_GC_MAX_SEGMENTS: usize = 8;
pub const DEFAULT_GC_NUM_SEGMENTS: usize = 1;
pub const DEFAULT_GC_MAX_BATCH_DELETES: usize = 256;
pub const DEFAULT_GC_MAX_SCAN_RECORDS: usize = 4096;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GcConfig {
pub enabled: bool,
pub scan_interval_ms: u64,
pub compaction_interval_ms: u64,
pub compaction_max_segments: usize,
pub compaction_num_segments: usize,
pub max_batch_deletes: usize,
pub max_scan_records: usize,
}
impl Default for GcConfig {
fn default() -> Self {
Self {
enabled: false,
scan_interval_ms: 0,
compaction_interval_ms: 0,
compaction_max_segments: DEFAULT_GC_MAX_SEGMENTS,
compaction_num_segments: DEFAULT_GC_NUM_SEGMENTS,
max_batch_deletes: DEFAULT_GC_MAX_BATCH_DELETES,
max_scan_records: DEFAULT_GC_MAX_SCAN_RECORDS,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct StoreConfig {
pub index_size: usize,
pub page_size: usize,
pub num_pages: usize,
pub mutable_fraction: f64,
pub max_sessions: usize,
pub bftree_path: Option<PathBuf>,
pub range_index_dir: Option<PathBuf>,
pub enable_revivification: bool,
pub enable_read_cache: bool,
pub read_cache_num_pages: usize,
pub gc: GcConfig,
}
impl Default for StoreConfig {
fn default() -> Self {
Self::minimal()
}
}
impl StoreConfig {
#[must_use]
pub fn auto() -> Self {
let sys_mem = detect_system_memory();
let budget = (sys_mem * DEFAULT_MEMORY_PERCENT / 100)
.clamp(MIN_MEMORY_BUDGET_BYTES, MAX_DEFAULT_MEMORY_BUDGET_BYTES);
Self::auto_with_budget(budget)
}
#[must_use]
pub fn auto_with_budget(memory_bytes: u64) -> Self {
let budget = memory_bytes.max(MIN_MEMORY_BUDGET_BYTES);
let page_size = DEFAULT_PAGE_SIZE;
let log_budget = budget * 3 / 4;
let target_pages =
((log_budget / (page_size as u64)) as usize).clamp(MIN_NUM_PAGES, MAX_NUM_PAGES);
let num_pages = target_pages.next_power_of_two();
let index_budget = budget / 8;
let target_buckets = (index_budget / 64) as usize;
let index_size = target_buckets
.next_power_of_two()
.clamp(MIN_INDEX_SIZE, MAX_INDEX_SIZE);
let cores = detect_cpu_cores();
let max_sessions = (cores * SESSIONS_PER_CORE)
.next_power_of_two()
.clamp(MIN_SESSIONS, MAX_SESSIONS);
Self {
index_size,
page_size,
num_pages,
mutable_fraction: DEFAULT_MUTABLE_FRACTION,
max_sessions,
bftree_path: None,
range_index_dir: None,
enable_revivification: false,
enable_read_cache: false,
read_cache_num_pages: DEFAULT_READ_CACHE_NUM_PAGES,
gc: GcConfig::default(),
}
}
#[must_use]
pub fn minimal() -> Self {
Self {
index_size: DEFAULT_INDEX_SIZE,
page_size: DEFAULT_PAGE_SIZE,
num_pages: DEFAULT_NUM_PAGES,
mutable_fraction: DEFAULT_MUTABLE_FRACTION,
max_sessions: DEFAULT_MAX_SESSIONS,
bftree_path: None,
range_index_dir: None,
enable_revivification: false,
enable_read_cache: false,
read_cache_num_pages: DEFAULT_READ_CACHE_NUM_PAGES,
gc: GcConfig::default(),
}
}
#[must_use]
pub fn recommended_index_size(expected_keys: u64) -> usize {
let buckets = (expected_keys.saturating_mul(2))
.div_ceil(INDEX_BUCKET_DATA_SLOTS as u64)
.min(MAX_INDEX_SIZE as u64);
usize::try_from(buckets).map_or(MAX_INDEX_SIZE, |b| {
b.next_power_of_two().clamp(MIN_INDEX_SIZE, MAX_INDEX_SIZE)
})
}
pub fn validate(&self) -> Result<()> {
if self.index_size == 0 || !self.index_size.is_power_of_two() {
let mut buf = Buffer::new();
let mut msg = String::from("index_size 必须为非零且为 2 的幂,当前为 ");
msg.push_str(buf.format(self.index_size));
msg.push_str(";索引打开时按此值定容且运行期无在线扩容(每桶 64B、7 数据槽/桶),容量不足将拉长碰撞链直至溢出桶池耗尽(OverflowPoolExhausted)显式拒绝写入;建议按预期键数 K 取 next_power_of_two(K×2/7) 预留 50% 负载余量,当前值就近建议 ");
let suggest = if self.index_size == 0 {
MIN_INDEX_SIZE
} else {
self.index_size.next_power_of_two()
};
msg.push_str(buf.format(suggest));
msg.push_str(" 桶");
return Err(Error::InvalidConfig(msg));
}
if !self.page_size.is_power_of_two() {
let mut msg = String::from("page_size 必须为 2 的幂,当前为 ");
let mut buf = Buffer::new();
msg.push_str(buf.format(self.page_size));
return Err(Error::InvalidConfig(msg));
}
if !self.page_size.is_multiple_of(SECTOR_ALIGNMENT) {
let mut msg = String::from("page_size 必须是 ");
let mut buf = Buffer::new();
msg.push_str(buf.format(SECTOR_ALIGNMENT));
msg.push_str(" 的整数倍,当前为 ");
msg.push_str(buf.format(self.page_size));
return Err(Error::InvalidConfig(msg));
}
if !self.num_pages.is_power_of_two() || self.num_pages == 0 {
let mut msg = String::from("num_pages 必须为非零且为 2 的幂,当前为 ");
let mut buf = Buffer::new();
msg.push_str(buf.format(self.num_pages));
return Err(Error::InvalidConfig(msg));
}
if self.mutable_fraction <= 0.0 || self.mutable_fraction > 1.0 {
let mut fmt_buf = FmtBuffer::new();
let mut msg = String::from("mutable_fraction 必须在 (0.0, 1.0] 区间内,当前为 ");
msg.push_str(fmt_buf.format(self.mutable_fraction));
return Err(Error::InvalidConfig(msg));
}
if !self.read_cache_num_pages.is_power_of_two() || self.read_cache_num_pages == 0 {
let mut msg = String::from("read_cache_num_pages 必须为非零且为 2 的幂,当前为 ");
let mut buf = Buffer::new();
msg.push_str(buf.format(self.read_cache_num_pages));
return Err(Error::InvalidConfig(msg));
}
if self.max_sessions == 0 {
return Err(Error::InvalidConfig("max_sessions 必须大于 0".into()));
}
Ok(())
}
pub fn new(
index_size: usize,
page_size: usize,
num_pages: usize,
mutable_fraction: f64,
) -> Result<Self> {
let config = Self {
index_size,
page_size,
num_pages,
mutable_fraction,
max_sessions: DEFAULT_MAX_SESSIONS,
bftree_path: None,
range_index_dir: None,
enable_revivification: false,
enable_read_cache: false,
read_cache_num_pages: DEFAULT_READ_CACHE_NUM_PAGES,
gc: GcConfig::default(),
};
config.validate()?;
Ok(config)
}
pub fn with_max_sessions(mut self, max_sessions: usize) -> Result<Self> {
self.max_sessions = max_sessions;
self.validate()?;
Ok(self)
}
pub fn with_revivification(mut self, enable: bool) -> Self {
self.enable_revivification = enable;
self
}
pub fn with_read_cache(mut self, enable: bool) -> Self {
self.enable_read_cache = enable;
self
}
pub fn with_read_cache_pages(mut self, num_pages: usize) -> Result<Self> {
self.read_cache_num_pages = num_pages;
self.validate()?;
Ok(self)
}
pub fn with_bftree_path(mut self, path: impl Into<PathBuf>) -> Self {
self.bftree_path = Some(path.into());
self
}
pub fn with_range_index_dir(mut self, path: impl Into<PathBuf>) -> Self {
self.range_index_dir = Some(path.into());
self
}
pub fn to_hlog_config(&self) -> Result<HybridLogConfig> {
HybridLogConfig::new(self.page_size, self.num_pages, self.mutable_fraction).map_err(Into::into)
}
}