use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex, OnceLock, Weak,
},
time::{Duration, Instant},
};
use rusqlite::Connection;
use super::{Result, ViewError, ViewStore};
impl ViewStore {
pub fn derived_path(&self, generation: &str) -> Result<PathBuf> {
super::validate_generation(generation)?;
Ok(self.view_dir().join(format!("derived-{generation}.sqlite")))
}
pub fn trigram_path(&self, generation: &str) -> Result<PathBuf> {
super::validate_generation(generation)?;
Ok(self.view_dir().join(format!("trigram-{generation}.bin")))
}
pub fn blob_references_by_generation(
&self,
) -> Result<std::collections::BTreeMap<String, std::collections::BTreeSet<[u8; 32]>>> {
let mut result = std::collections::BTreeMap::new();
for entry in fs::read_dir(self.view_dir())? {
let entry = entry?;
let name = entry.file_name();
let Some(generation) = name
.to_str()
.and_then(|name| name.strip_prefix("manifest-"))
.and_then(|name| name.strip_suffix(".json"))
else {
continue;
};
let manifest = self.load_manifest(generation)?;
let keys = manifest
.plane_keys()
.filter_map(|(_, key)| {
if key.len() != 64 {
return None;
}
let bytes = (0..64)
.step_by(2)
.map(|offset| u8::from_str_radix(&key[offset..offset + 2], 16).ok())
.collect::<Option<Vec<_>>>()?;
bytes.try_into().ok()
})
.collect();
result.insert(generation.to_owned(), keys);
}
Ok(result)
}
pub fn sweep_generations(&self) -> Result<usize> {
let current = self.current_generation()?;
let mut generations = std::collections::BTreeSet::new();
for entry in fs::read_dir(self.view_dir())? {
let entry = entry?;
let name = entry.file_name();
let Some(name) = name.to_str() else { continue };
let generation = name
.strip_prefix("derived-")
.and_then(|s| s.strip_suffix(".sqlite"))
.or_else(|| {
name.strip_prefix("manifest-")
.and_then(|s| s.strip_suffix(".json"))
})
.or_else(|| {
name.strip_prefix("trigram-")
.and_then(|s| s.strip_suffix(".bin"))
})
.or_else(|| {
name.strip_prefix(".manifest-")
.and_then(|s| s.split_once(".json.tmp.").map(|(generation, _)| generation))
});
if let Some(generation) = generation {
generations.insert(generation.to_owned());
}
}
let mut removed = 0;
for generation in generations {
if current.as_deref() == Some(&generation) {
continue;
}
let (metadata_path, keys_path) = crate::pins::pin_paths(self.view_dir(), &generation);
if metadata_path.exists() {
let Ok(bytes) = fs::read(&metadata_path) else {
continue;
};
let Ok(metadata) = serde_json::from_slice::<crate::pins::PinMetadata>(&bytes)
else {
continue;
};
if crate::pins::owner_is_live(&metadata.owner)
&& crate::pins::now_ms().saturating_sub(metadata.renewed_at)
<= crate::pins::PIN_TTL_MS
{
continue;
}
let _ = fs::remove_file(metadata_path);
let _ = fs::remove_file(keys_path);
}
if crate::root_cache::sweep_read_markers(self.view_dir(), &generation).protected {
continue;
}
if self.current_generation()?.as_deref() == Some(&generation) {
continue;
}
self.remove_generation_files(&generation);
removed += 1;
}
Ok(removed)
}
pub(super) fn remove_generation_files(&self, generation: &str) {
if super::validate_generation(generation).is_err() {
return;
}
let temporary_prefix = format!(".manifest-{generation}.json.tmp.");
if let Ok(entries) = fs::read_dir(self.view_dir()) {
for entry in entries.flatten() {
if entry
.file_name()
.to_str()
.is_some_and(|name| name.starts_with(&temporary_prefix))
{
let _ = fs::remove_file(entry.path());
}
}
}
for path in [
self.derived_path(generation),
self.trigram_path(generation),
self.manifest_path(generation),
]
.into_iter()
.flatten()
{
for suffix in ["", "-wal", "-shm"] {
let mut name = path.as_os_str().to_owned();
name.push(suffix);
let _ = fs::remove_file(PathBuf::from(name));
}
}
}
}
#[derive(Clone)]
struct DeferredCheckpointJob {
path: PathBuf,
cancelled: Arc<AtomicBool>,
}
static DEFERRED_CHECKPOINTS: OnceLock<Mutex<HashMap<PathBuf, DeferredCheckpointJob>>> =
OnceLock::new();
static CHECKPOINT_LOCKS: OnceLock<Mutex<HashMap<PathBuf, Weak<Mutex<()>>>>> = OnceLock::new();
const DEFERRED_CHECKPOINT_IDLE_DELAY: Duration = Duration::from_millis(250);
fn checkpoint_lock(path: &Path) -> Arc<Mutex<()>> {
let mut locks = CHECKPOINT_LOCKS
.get_or_init(|| Mutex::new(HashMap::new()))
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(lock) = locks.get(path).and_then(Weak::upgrade) {
return lock;
}
let lock = Arc::new(Mutex::new(()));
locks.insert(path.to_path_buf(), Arc::downgrade(&lock));
lock
}
fn checkpoint_derived(path: &Path, connection: Option<&Connection>) -> Result<()> {
let lock = checkpoint_lock(path);
let _guard = lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let owned;
let connection = if let Some(connection) = connection {
connection
} else {
owned = Connection::open(path)?;
&owned
};
connection.busy_timeout(Duration::from_secs(5))?;
connection.pragma_update(None, "synchronous", "FULL")?;
let (busy, log_frames, checkpointed_frames): (i64, i64, i64) =
connection.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?))
})?;
if busy != 0 {
return Err(ViewError::InvalidManifest(format!(
"derived WAL checkpoint remained busy: path={} log_frames={} checkpointed_frames={}",
path.display(),
log_frames,
checkpointed_frames
)));
}
super::sync_file(path)?;
super::sync_parent(path)?;
Ok(())
}
pub(super) fn schedule_derived_checkpoint(path: PathBuf, connection: Connection) {
let key = path.parent().unwrap_or(&path).to_path_buf();
let cancelled = Arc::new(AtomicBool::new(false));
let job = DeferredCheckpointJob {
path: path.clone(),
cancelled: Arc::clone(&cancelled),
};
if let Some(previous) = DEFERRED_CHECKPOINTS
.get_or_init(|| Mutex::new(HashMap::new()))
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(key.clone(), job)
{
previous.cancelled.store(true, Ordering::Release);
log::debug!(
"view derived checkpoint superseded path={}",
previous.path.display()
);
}
let path_for_error = path.clone();
let key_for_error = key.clone();
let cancelled_for_error = Arc::clone(&cancelled);
let spawn = std::thread::Builder::new()
.name("aft-view-checkpoint".to_owned())
.spawn(move || {
std::thread::sleep(DEFERRED_CHECKPOINT_IDLE_DELAY);
let started = Instant::now();
if !cancelled.load(Ordering::Acquire) {
match checkpoint_derived(&path, Some(&connection)) {
Ok(()) => log::info!(
"view derived checkpoint completed ms={} path={}",
started.elapsed().as_millis(),
path.display()
),
Err(error) => log::warn!(
"view derived checkpoint deferred to next clone path={} error={}",
path.display(),
error
),
}
}
let mut jobs = DEFERRED_CHECKPOINTS
.get_or_init(|| Mutex::new(HashMap::new()))
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if jobs
.get(&key)
.is_some_and(|current| Arc::ptr_eq(¤t.cancelled, &cancelled))
{
jobs.remove(&key);
}
});
if let Err(error) = spawn {
let mut jobs = DEFERRED_CHECKPOINTS
.get_or_init(|| Mutex::new(HashMap::new()))
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if jobs
.get(&key_for_error)
.is_some_and(|current| Arc::ptr_eq(¤t.cancelled, &cancelled_for_error))
{
jobs.remove(&key_for_error);
}
log::warn!(
"view derived checkpoint worker unavailable; next clone will checkpoint path={} error={}",
path_for_error.display(),
error
);
}
}
pub(super) fn clone_derived(source: &Path, destination: &Path) -> Result<()> {
checkpoint_derived(source, None)?;
let started = Instant::now();
let mechanism = if try_clone(source, destination) {
if cfg!(target_os = "macos") {
"clonefile"
} else {
"reflink"
}
} else {
let _ = fs::remove_file(destination);
fs::copy(source, destination)?;
"copy"
};
log::info!(
"view derived clone mechanism={} ms={} source={} destination={}",
mechanism,
started.elapsed().as_millis(),
source.display(),
destination.display()
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deferred_checkpoint_runs_after_the_publication_path_returns() {
let directory = tempfile::tempdir().unwrap();
let source = directory.path().join("source.sqlite");
let connection = Connection::open(&source).unwrap();
connection
.pragma_update(None, "journal_mode", "WAL")
.unwrap();
connection
.pragma_update(None, "synchronous", "FULL")
.unwrap();
connection
.pragma_update(None, "wal_autocheckpoint", 0)
.unwrap();
connection
.execute_batch(
"CREATE TABLE state (value TEXT NOT NULL);\
INSERT INTO state VALUES ('durable');",
)
.unwrap();
let wal = PathBuf::from(format!("{}-wal", source.display()));
assert!(fs::metadata(&wal).unwrap().len() > 0);
schedule_derived_checkpoint(source.clone(), connection);
assert!(
fs::metadata(&wal).is_ok_and(|metadata| metadata.len() > 0),
"checkpoint ran synchronously on the publication path"
);
let deadline = Instant::now() + Duration::from_secs(5);
while fs::metadata(&wal).is_ok_and(|metadata| metadata.len() > 0) {
assert!(
Instant::now() < deadline,
"detached derived checkpoint did not finish"
);
std::thread::sleep(Duration::from_millis(10));
}
assert_eq!(
Connection::open(&source)
.unwrap()
.query_row("SELECT value FROM state", [], |row| row.get::<_, String>(0))
.unwrap(),
"durable"
);
}
#[test]
fn clone_checkpoints_committed_wal_before_copying_the_main_file() {
let directory = tempfile::tempdir().unwrap();
let source = directory.path().join("source.sqlite");
let destination = directory.path().join("destination.sqlite");
let connection = Connection::open(&source).unwrap();
connection
.pragma_update(None, "journal_mode", "WAL")
.unwrap();
connection
.pragma_update(None, "synchronous", "FULL")
.unwrap();
connection
.pragma_update(None, "wal_autocheckpoint", 0)
.unwrap();
connection
.execute_batch(
"CREATE TABLE state (value TEXT NOT NULL);\
INSERT INTO state VALUES ('committed-in-wal');",
)
.unwrap();
let wal = PathBuf::from(format!("{}-wal", source.display()));
assert!(fs::metadata(&wal).unwrap().len() > 0);
clone_derived(&source, &destination).unwrap();
assert_eq!(
Connection::open(&destination)
.unwrap()
.query_row("SELECT value FROM state", [], |row| row.get::<_, String>(0))
.unwrap(),
"committed-in-wal"
);
}
}
#[cfg(target_os = "macos")]
fn try_clone(source: &Path, destination: &Path) -> bool {
use std::{ffi::CString, os::unix::ffi::OsStrExt};
let (Ok(source), Ok(destination)) = (
CString::new(source.as_os_str().as_bytes()),
CString::new(destination.as_os_str().as_bytes()),
) else {
return false;
};
unsafe { libc::clonefile(source.as_ptr(), destination.as_ptr(), 0) == 0 }
}
#[cfg(target_os = "linux")]
fn try_clone(source: &Path, destination: &Path) -> bool {
use std::os::fd::AsRawFd;
let (Ok(source), Ok(destination)) = (
fs::File::open(source),
fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(destination),
) else {
return false;
};
unsafe {
libc::ioctl(
destination.as_raw_fd(),
0x40049409 as libc::c_ulong,
source.as_raw_fd(),
) == 0
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn try_clone(_source: &Path, _destination: &Path) -> bool {
false
}