use std::ffi::CStr;
use std::fmt;
use std::os::raw::c_int;
use std::sync::{Once, OnceLock};
use rusqlite::ffi;
pub const REQUIRED_LIBSQLITE3_FLAGS: &str = "-DSQLITE_DEFAULT_MEMSTATUS=0 \
-USQLITE_ENABLE_MEMORY_MANAGEMENT -DSQLITE_DISABLE_PAGECACHE_OVERFLOW_STATS";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SqliteLockProfile {
pub memory_statistics: bool,
pub shared_page_cache: bool,
pub page_cache_overflow_statistics: bool,
}
impl SqliteLockProfile {
pub const REQUIRED: Self = Self {
memory_statistics: false,
shared_page_cache: false,
page_cache_overflow_statistics: false,
};
#[must_use]
pub fn compiled() -> Self {
Self {
memory_statistics: !compile_option_used(c"DEFAULT_MEMSTATUS=0"),
shared_page_cache: compile_option_used(c"ENABLE_MEMORY_MANAGEMENT"),
page_cache_overflow_statistics: !compile_option_used(
c"DISABLE_PAGECACHE_OVERFLOW_STATS",
),
}
}
#[must_use]
pub fn effective() -> Self {
let mut profile = Self::compiled();
if memory_statistics_outcome() == Some(MemoryStatisticsOutcome::Applied) {
profile.memory_statistics = false;
}
profile
}
#[must_use]
pub fn is_required(self) -> bool {
self == Self::REQUIRED
}
}
impl fmt::Display for SqliteLockProfile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let state = |on: bool| if on { "on" } else { "off" };
write!(
f,
"memory statistics {}, shared page cache {}, page-cache overflow statistics {}",
state(self.memory_statistics),
state(self.shared_page_cache),
state(self.page_cache_overflow_statistics),
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error(
"SQLite runs with process-wide locks ({effective}); build with \
LIBSQLITE3_FLAGS=\"{REQUIRED_LIBSQLITE3_FLAGS}\""
)]
pub struct SqliteLockProfileMismatch {
pub effective: SqliteLockProfile,
}
pub fn check_lock_profile() -> Result<(), SqliteLockProfileMismatch> {
let effective = SqliteLockProfile::effective();
if effective.is_required() {
Ok(())
} else {
Err(SqliteLockProfileMismatch { effective })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MemoryStatisticsOutcome {
Applied,
AlreadyOff,
TooLate,
}
impl fmt::Display for MemoryStatisticsOutcome {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Applied => "applied",
Self::AlreadyOff => "already off",
Self::TooLate => "too late",
})
}
}
static MEMORY_STATISTICS_OUTCOME: OnceLock<MemoryStatisticsOutcome> = OnceLock::new();
#[allow(unsafe_code)]
pub unsafe fn disable_memory_statistics_at_process_start() -> MemoryStatisticsOutcome {
*MEMORY_STATISTICS_OUTCOME.get_or_init(|| {
let rc = unsafe { ffi::sqlite3_config(ffi::SQLITE_CONFIG_MEMSTATUS, 0 as c_int) };
if rc != ffi::SQLITE_OK {
MemoryStatisticsOutcome::TooLate
} else if SqliteLockProfile::compiled().memory_statistics {
MemoryStatisticsOutcome::Applied
} else {
MemoryStatisticsOutcome::AlreadyOff
}
})
}
#[must_use]
pub fn memory_statistics_outcome() -> Option<MemoryStatisticsOutcome> {
MEMORY_STATISTICS_OUTCOME.get().copied()
}
static FIRST_OPEN: Once = Once::new();
pub(crate) fn check_at_first_open() {
FIRST_OPEN.call_once(|| {
if let Some(warning) =
lock_profile_warning(SqliteLockProfile::effective(), memory_statistics_outcome())
{
tracing::warn!("{warning}");
}
});
}
fn lock_profile_warning(
effective: SqliteLockProfile,
switch: Option<MemoryStatisticsOutcome>,
) -> Option<String> {
if effective.is_required() {
return None;
}
let switch = switch.map_or_else(|| "not called".to_string(), |outcome| outcome.to_string());
Some(format!(
"{}; concurrent connections in this process queue on these locks \
(allocator statistics switch at process start: {switch})",
SqliteLockProfileMismatch { effective }
))
}
fn compile_option_used(name: &CStr) -> bool {
#[allow(unsafe_code)]
let used = unsafe { ffi::sqlite3_compileoption_used(name.as_ptr()) };
used != 0
}
#[cfg(test)]
pub(crate) fn checked_at_first_open() -> bool {
FIRST_OPEN.is_completed()
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn first_open_warning_names_the_build_setting_unless_the_profile_is_required() {
assert_eq!(
lock_profile_warning(SqliteLockProfile::REQUIRED, None),
None
);
let shared_page_cache = SqliteLockProfile {
shared_page_cache: true,
..SqliteLockProfile::REQUIRED
};
let warning =
lock_profile_warning(shared_page_cache, Some(MemoryStatisticsOutcome::Applied))
.expect("a shared page cache is warned about");
for expected in [
"memory statistics off, shared page cache on, page-cache overflow statistics off",
&format!("LIBSQLITE3_FLAGS=\"{REQUIRED_LIBSQLITE3_FLAGS}\""),
"allocator statistics switch at process start: applied",
] {
assert!(warning.contains(expected), "{expected:?} in {warning:?}");
}
let memory_statistics = SqliteLockProfile {
memory_statistics: true,
..SqliteLockProfile::REQUIRED
};
let warning = lock_profile_warning(memory_statistics, None)
.expect("allocator statistics are warned about");
assert!(
warning.contains("allocator statistics switch at process start: not called"),
"{warning:?}"
);
}
}