use super::*;
pub(crate) const TEST_HARNESS_ENV: &str = "KHIVE_TEST_HARNESS";
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum WalCeilingSource {
BackendField,
Environment,
#[default]
Default,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct WalCeilingPolicy {
pub bytes: u64,
pub source: WalCeilingSource,
}
impl WalCeilingPolicy {
pub fn validate_static(
self,
file_backed: bool,
wal_mode: bool,
read_only: bool,
) -> Result<(), SqliteError> {
if self.bytes == 0 {
return Ok(());
}
if i64::try_from(self.bytes).is_err() {
return Err(SqliteError::WalCeilingOffsetOverflow { bytes: self.bytes });
}
if read_only {
return Ok(());
}
if !file_backed {
return Err(SqliteError::WalCeilingUnsupported {
bytes: self.bytes,
backend_kind: "in-memory backend",
});
}
if !wal_mode {
return Err(SqliteError::WalCeilingUnsupported {
bytes: self.bytes,
backend_kind: "non-WAL backend",
});
}
Ok(())
}
pub fn effective_bytes(self, read_only: bool) -> u64 {
if read_only {
0
} else {
self.bytes
}
}
}
#[derive(Clone, Debug)]
pub struct PoolConfig {
pub path: Option<PathBuf>,
pub code_map_vfs: Option<String>,
#[cfg(any(unix, windows))]
pub expected_file_identity: Option<DatabaseFileIdentity>,
pub max_readers: usize,
pub reader_max_age: Duration,
pub reader_max_ops: u64,
pub wal_mode: bool,
pub busy_timeout: Duration,
pub checkout_timeout: Duration,
pub reader_checkout_warn_after: Duration,
pub journal_size_limit_bytes: i64,
pub read_only: bool,
pub wal_ceiling: WalCeilingPolicy,
pub write_queue_enabled: Option<bool>,
pub write_queue_capacity: usize,
pub write_routing_strict: bool,
pub write_admission_deadline_ms: u64,
pub disk_guard_config: Option<EffectiveDiskGuardConfig>,
pub volume_lock_dir: Option<PathBuf>,
pub read_tx_max_age: Duration,
}
const WRITE_ADMISSION_DEADLINE_MS_RANGE: std::ops::RangeInclusive<u64> = 100..=10_000;
const DEFAULT_WRITE_ADMISSION_DEADLINE_MS: u64 = 2000;
impl Default for PoolConfig {
fn default() -> Self {
Self {
path: None,
code_map_vfs: None,
#[cfg(any(unix, windows))]
expected_file_identity: None,
max_readers: std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
.clamp(1, DEFAULT_READER_CAP),
reader_max_age: Duration::from_secs(crate::env::env_parse_or(
"KHIVE_READER_MAX_AGE_SECS",
300,
)),
reader_max_ops: crate::env::env_parse_or("KHIVE_READER_MAX_OPS", 5000),
wal_mode: true,
busy_timeout: Duration::from_secs(crate::env::env_parse_or(
"KHIVE_BUSY_TIMEOUT_SECS",
30,
)),
checkout_timeout: Duration::from_secs(crate::env::env_parse_or(
"KHIVE_CHECKOUT_TIMEOUT_SECS",
5,
)),
reader_checkout_warn_after: Duration::from_secs(crate::env::env_parse_or(
"KHIVE_READER_CHECKOUT_WARN_SECS",
10,
)),
journal_size_limit_bytes: crate::env::env_parse_or(
"KHIVE_JOURNAL_SIZE_LIMIT_BYTES",
DEFAULT_JOURNAL_SIZE_LIMIT_BYTES,
),
read_only: false,
wal_ceiling: WalCeilingPolicy::default(),
write_queue_enabled: std::env::var_os("KHIVE_WRITE_QUEUE").map(|v| {
v.to_str()
.is_some_and(|v| v == "1" || v.eq_ignore_ascii_case("true"))
}),
write_queue_capacity: {
let capacity = crate::env::env_parse_or(
"KHIVE_WRITE_QUEUE_CAPACITY",
DEFAULT_WRITE_QUEUE_CAPACITY,
);
if capacity > 0 {
capacity
} else {
DEFAULT_WRITE_QUEUE_CAPACITY
}
},
write_routing_strict: std::env::var("KHIVE_WRITE_ROUTING")
.map(|v| v.eq_ignore_ascii_case("strict"))
.unwrap_or(false),
write_admission_deadline_ms: crate::env::env_parse_or(
"KHIVE_WRITE_ADMISSION_DEADLINE_MS",
DEFAULT_WRITE_ADMISSION_DEADLINE_MS,
),
disk_guard_config: None,
#[cfg(test)]
volume_lock_dir: Some(test_volume_lock_dir()),
#[cfg(not(test))]
volume_lock_dir: crate::default_volume_lock_dir().ok(),
read_tx_max_age: crate::checkpoint::tx_age_thresholds_from_env(
Duration::from_secs(30),
Duration::from_secs(120),
)
.1,
}
}
}
#[cfg(any(test, feature = "test-support"))]
impl PoolConfig {
pub fn for_test() -> Self {
Self {
max_readers: 2,
volume_lock_dir: Some(test_volume_lock_dir()),
..Self::default()
}
}
}
pub(super) fn refuse_home_data_store_in_tests(config: &PoolConfig) -> Result<(), SqliteError> {
if std::env::var(TEST_HARNESS_ENV).as_deref() != Ok("1") {
return Ok(());
}
let Some(path) = config.path.as_deref() else {
return Ok(());
};
if path
.as_os_str()
.as_encoded_bytes()
.get(..5)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"file:"))
{
return Err(SqliteError::InvalidData(format!(
"test harness refused SQLite URI database path {}; use a filesystem path outside \
HOME/.khive (deliberate sessions against a real store run the built binary \
directly, outside the Cargo test environment)",
path.display()
)));
}
let Some(home) = std::env::var_os("HOME") else {
return Ok(());
};
let canonical_path = canonicalize_deepest_existing(path)?;
let canonical_home_data_dir =
canonicalize_deepest_existing(&PathBuf::from(home).join(".khive"))?;
if canonical_path.starts_with(&canonical_home_data_dir) {
return Err(SqliteError::InvalidData(format!(
"test harness refused to open SQLite database under HOME/.khive: {} \
(deliberate sessions against a real store run the built binary directly, \
outside the Cargo test environment)",
canonical_path.display()
)));
}
Ok(())
}
fn canonicalize_deepest_existing(path: &Path) -> Result<PathBuf, SqliteError> {
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir().map_err(SqliteError::Io)?.join(path)
};
for ancestor in absolute.ancestors() {
match fs::canonicalize(ancestor) {
Ok(mut canonical) => {
let missing = absolute.strip_prefix(ancestor).map_err(|error| {
SqliteError::InvalidData(format!(
"failed to preserve missing path components for {}: {error}",
absolute.display()
))
})?;
canonical.push(missing);
return Ok(canonical);
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => {
return Err(SqliteError::InvalidData(format!(
"failed to canonicalize database path ancestor {}: {error}",
ancestor.display()
)));
}
}
}
Err(SqliteError::InvalidData(format!(
"database path has no canonicalizable ancestor: {}",
absolute.display()
)))
}
pub(super) fn validate_write_admission_deadline(deadline_ms: u64) -> Result<(), SqliteError> {
if WRITE_ADMISSION_DEADLINE_MS_RANGE.contains(&deadline_ms) {
return Ok(());
}
Err(SqliteError::InvalidConfig(format!(
"write_admission_deadline_ms must be in [{}, {}] ms, got {deadline_ms}",
WRITE_ADMISSION_DEADLINE_MS_RANGE.start(),
WRITE_ADMISSION_DEADLINE_MS_RANGE.end()
)))
}