use std::collections::HashMap;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use std::time::{SystemTime, UNIX_EPOCH};
use memmap2::Mmap;
use parking_lot::Mutex;
use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
use std::os::unix::fs::MetadataExt;
pub const SHARD_MAGIC: u32 = 0x5A52414C;
pub const SHARD_FORMAT_VERSION: u32 = 6;
#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone)]
#[archive(check_bytes)]
pub struct ShardHeader {
pub magic: u32,
pub format_version: u32,
pub zshrs_version: String,
pub pointer_width: u32,
pub built_at_secs: u64,
}
#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone)]
#[archive(check_bytes)]
pub struct AutoloadEntry {
pub binary_mtime_at_cache: i64,
pub binary_len_at_cache: u64,
pub cached_at_secs: i64,
pub source_dir: String,
pub source_sha: [u8; 32],
pub chunk_blob: Vec<u8>,
pub resolved: Option<ResolvedLoad>,
}
#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone, PartialEq)]
#[archive(check_bytes)]
pub struct ResolvedLoad {
pub body: String,
pub registered: String,
pub dump_ksh: i32,
pub from_wordcode: bool,
pub parse_failed: bool,
pub ksh_style: bool,
pub stamps: Vec<FileStamp>,
}
#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone, PartialEq)]
#[archive(check_bytes)]
pub struct FileStamp {
pub path: String,
pub exists: bool,
pub mtime_ns: i64,
pub len: u64,
}
#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone)]
#[archive(check_bytes)]
pub struct AutoloadShard {
pub header: ShardHeader,
pub entries: HashMap<String, AutoloadEntry>,
}
pub struct MmappedShard {
_mmap: Mmap,
archived: *const ArchivedAutoloadShard,
}
unsafe impl Send for MmappedShard {}
unsafe impl Sync for MmappedShard {}
impl MmappedShard {
pub fn open(path: &Path) -> Option<Self> {
let file = File::open(path).ok()?;
let mmap = unsafe { Mmap::map(&file).ok()? };
let archived = rkyv::check_archived_root::<AutoloadShard>(&mmap[..]).ok()?;
let archived_ptr = archived as *const ArchivedAutoloadShard;
Some(Self {
_mmap: mmap,
archived: archived_ptr,
})
}
fn shard(&self) -> &ArchivedAutoloadShard {
unsafe { &*self.archived }
}
fn header_ok(&self) -> bool {
let h = &self.shard().header;
let magic: u32 = h.magic.into();
let fv: u32 = h.format_version.into();
let pw: u32 = h.pointer_width.into();
magic == SHARD_MAGIC
&& fv == SHARD_FORMAT_VERSION
&& pw as usize == std::mem::size_of::<usize>()
&& h.zshrs_version.as_str() == env!("CARGO_PKG_VERSION")
}
fn lookup(&self, name: &str) -> Option<&ArchivedAutoloadEntry> {
self.shard().entries.get(name)
}
}
fn entry_binary_matches(entry: &ArchivedAutoloadEntry) -> bool {
let Some((mtime, len)) = current_binary_identity() else {
return false;
};
let cached_mtime: i64 = entry.binary_mtime_at_cache.into();
let cached_len: u64 = entry.binary_len_at_cache.into();
cached_mtime == mtime && cached_len == len
}
pub struct AutoloadCache {
path: PathBuf,
lock_path: PathBuf,
mmap: Mutex<Option<MmappedShard>>,
pending: Mutex<Vec<(String, Vec<u8>, String, [u8; 32])>>,
resolved_pending: Mutex<HashMap<String, ResolvedLoad>>,
}
const PENDING_FLUSH_MAX: usize = 256;
impl AutoloadCache {
pub fn open(path: &Path) -> std::io::Result<Self> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let parent = path.parent().unwrap_or_else(|| Path::new("/tmp"));
let lock_path = parent.join(format!(
"{}.lock",
path.file_name()
.and_then(|s| s.to_str())
.unwrap_or("autoloads.rkyv")
));
Ok(Self {
path: path.to_path_buf(),
lock_path,
mmap: Mutex::new(None),
pending: Mutex::new(Vec::new()),
resolved_pending: Mutex::new(HashMap::new()),
})
}
fn ensure_mmap(&self) {
let mut guard = self.mmap.lock();
if guard.is_none() {
*guard = MmappedShard::open(&self.path);
}
}
fn invalidate_mmap(&self) {
let mut guard = self.mmap.lock();
*guard = None;
}
pub fn get(&self, name: &str) -> Option<Vec<u8>> {
if let Some(blob) = self.pending_lookup(name, None, None) {
return (!blob.is_empty()).then_some(blob);
}
self.ensure_mmap();
let guard = self.mmap.lock();
let shard = guard.as_ref()?;
if !shard.header_ok() {
return None;
}
let entry = shard.lookup(name)?;
if !entry_binary_matches(entry) {
return None;
}
(!entry.chunk_blob.is_empty()).then(|| entry.chunk_blob.as_slice().to_vec())
}
pub fn get_for_source(
&self,
name: &str,
source_dir: &str,
source_sha: &[u8; 32],
) -> Option<Vec<u8>> {
if let Some(blob) = self.pending_lookup(name, Some(source_dir), Some(source_sha)) {
return Some(blob);
}
self.ensure_mmap();
let guard = self.mmap.lock();
let shard = guard.as_ref()?;
if !shard.header_ok() {
return None;
}
let entry = shard.lookup(name)?;
if !entry_binary_matches(entry) {
return None;
}
if entry.source_dir.as_str() != source_dir {
return None;
}
if entry.source_sha.as_slice() != source_sha.as_slice() {
return None;
}
Some(entry.chunk_blob.as_slice().to_vec())
}
fn owned_shard_for_write(&self) -> AutoloadShard {
match read_owned_shard(&self.path) {
Some(s)
if s.header.zshrs_version == env!("CARGO_PKG_VERSION")
&& s.header.pointer_width as usize == std::mem::size_of::<usize>()
&& s.header.format_version == SHARD_FORMAT_VERSION =>
{
s
}
_ => fresh_shard(),
}
}
pub fn put_one(
&self,
name: &str,
chunk_blob: Vec<u8>,
source_dir: &str,
source_sha: [u8; 32],
) -> Result<(), String> {
{
let mut pending = self.pending.lock();
pending.push((
name.to_string(),
chunk_blob,
source_dir.to_string(),
source_sha,
));
if pending.len() < PENDING_FLUSH_MAX {
return Ok(());
}
}
self.flush_pending()
}
pub fn flush_pending(&self) -> Result<(), String> {
let batch = {
let mut pending = self.pending.lock();
if pending.is_empty() && self.resolved_pending.lock().is_empty() {
return Ok(());
}
std::mem::take(&mut *pending)
};
let Some(lock) = try_acquire_lock(&self.lock_path) else {
self.requeue(batch);
return Ok(());
};
self.put_many_locked(&batch, lock)
}
fn requeue(&self, mut batch: Vec<(String, Vec<u8>, String, [u8; 32])>) {
let mut pending = self.pending.lock();
batch.append(&mut pending);
let excess = batch.len().saturating_sub(PENDING_FLUSH_MAX);
batch.drain(..excess);
*pending = batch;
}
fn pending_lookup(
&self,
name: &str,
source_dir: Option<&str>,
source_sha: Option<&[u8; 32]>,
) -> Option<Vec<u8>> {
let pending = self.pending.lock();
pending.iter().rev().find_map(|(n, blob, dir, sha)| {
if n != name {
return None;
}
if source_dir.is_some_and(|d| d != dir) {
return None;
}
if source_sha.is_some_and(|s| s != sha) {
return None;
}
Some(blob.clone())
})
}
pub fn put_many(&self, entries: &[(String, Vec<u8>, String, [u8; 32])]) -> Result<(), String> {
if entries.is_empty() && self.resolved_pending.lock().is_empty() {
return Ok(());
}
let Some(lock) = acquire_lock(&self.lock_path) else {
return Ok(());
};
self.put_many_locked(entries, lock)
}
fn put_many_locked(
&self,
entries: &[(String, Vec<u8>, String, [u8; 32])],
_lock: nix::fcntl::Flock<File>,
) -> Result<(), String> {
let mut shard = self.owned_shard_for_write();
let (bin_mtime, bin_len) = current_binary_identity().unwrap_or((0, 0));
let now = now_secs();
for (name, chunk_blob, source_dir, source_sha) in entries {
if chunk_blob.is_empty() && shard.entries.contains_key(name) {
continue;
}
let noted = self
.resolved_pending
.lock()
.remove(name)
.filter(|r| r.registered.is_empty() || registered_digest(r) == *source_sha);
let prior = shard
.entries
.get(name)
.filter(|e| e.source_dir == *source_dir && e.source_sha == *source_sha)
.and_then(|e| e.resolved.clone());
shard.entries.insert(
name.clone(),
AutoloadEntry {
binary_mtime_at_cache: bin_mtime,
binary_len_at_cache: bin_len,
cached_at_secs: now,
source_dir: source_dir.clone(),
source_sha: *source_sha,
chunk_blob: chunk_blob.clone(),
resolved: noted.or(prior),
},
);
}
for (name, r) in std::mem::take(&mut *self.resolved_pending.lock()) {
if let Some(e) = shard.entries.get_mut(&name) {
let same_dir = r
.stamps
.first()
.is_some_and(|s| s.path == format!("{}/{}", e.source_dir, name));
let fits = if r.registered.is_empty() {
e.resolved.is_none()
} else {
registered_digest(&r) == e.source_sha
};
if same_dir && fits {
e.resolved = Some(r);
}
}
}
shard.header.built_at_secs = now as u64;
write_shard_atomic(&self.path, &shard)?;
self.invalidate_mmap();
Ok(())
}
pub fn remove(&self, name: &str) -> Result<(), String> {
self.pending.lock().retain(|(n, _, _, _)| n != name);
self.resolved_pending.lock().remove(name);
let _lock = match acquire_lock(&self.lock_path) {
Some(l) => l,
None => return Ok(()),
};
let mut shard = self.owned_shard_for_write();
if shard.entries.remove(name).is_none() {
return Ok(());
}
shard.header.built_at_secs = now_secs() as u64;
write_shard_atomic(&self.path, &shard)?;
self.invalidate_mmap();
Ok(())
}
pub fn entry_count(&self) -> usize {
self.ensure_mmap();
let guard = self.mmap.lock();
guard.as_ref().map(|s| s.shard().entries.len()).unwrap_or(0)
}
pub fn cached_names(&self) -> std::collections::HashSet<String> {
self.ensure_mmap();
let guard = self.mmap.lock();
let Some(shard) = guard.as_ref() else {
return std::collections::HashSet::new();
};
shard
.shard()
.entries
.keys()
.map(|k| k.as_str().to_string())
.collect()
}
pub fn stats(&self) -> (i64, i64) {
self.ensure_mmap();
let guard = self.mmap.lock();
let Some(shard) = guard.as_ref() else {
return (0, 0);
};
let count = shard.shard().entries.len() as i64;
let bytes: i64 = shard
.shard()
.entries
.values()
.map(|e| e.chunk_blob.len() as i64)
.sum();
(count, bytes)
}
pub fn clear(&self) -> std::io::Result<()> {
let _lock = acquire_lock(&self.lock_path);
let res = match std::fs::remove_file(&self.path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e),
};
crate::atomic_write::reap_orphan_temps(&self.path);
self.invalidate_mmap();
res
}
}
fn acquire_lock(path: &Path) -> Option<nix::fcntl::Flock<File>> {
let f = File::options()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)
.ok()?;
nix::fcntl::Flock::lock(f, nix::fcntl::FlockArg::LockExclusive).ok()
}
fn try_acquire_lock(path: &Path) -> Option<nix::fcntl::Flock<File>> {
let f = File::options()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)
.ok()?;
nix::fcntl::Flock::lock(f, nix::fcntl::FlockArg::LockExclusiveNonblock).ok()
}
fn fresh_shard() -> AutoloadShard {
AutoloadShard {
header: ShardHeader {
magic: SHARD_MAGIC,
format_version: SHARD_FORMAT_VERSION,
zshrs_version: env!("CARGO_PKG_VERSION").to_string(),
pointer_width: std::mem::size_of::<usize>() as u32,
built_at_secs: now_secs() as u64,
},
entries: HashMap::new(),
}
}
fn read_owned_shard(path: &Path) -> Option<AutoloadShard> {
let bytes = std::fs::read(path).ok()?;
let archived = rkyv::check_archived_root::<AutoloadShard>(&bytes[..]).ok()?;
archived.deserialize(&mut rkyv::Infallible).ok()
}
fn write_shard_atomic(path: &Path, shard: &AutoloadShard) -> Result<(), String> {
let bytes = rkyv::to_bytes::<_, 4096>(shard).map_err(|e| format!("rkyv serialize: {}", e))?;
crate::atomic_write::write_bytes_atomic(path, &bytes)
}
fn now_secs() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
fn current_binary_identity() -> Option<(i64, u64)> {
static BIN_ID: OnceLock<Option<(i64, u64)>> = OnceLock::new();
*BIN_ID.get_or_init(|| {
let exe = std::env::current_exe().ok()?;
let meta = std::fs::metadata(&exe).ok()?;
Some((meta.mtime(), meta.len()))
})
}
pub fn source_digest(text: &str) -> [u8; 32] {
use sha2::Digest;
let mut hasher = sha2::Sha256::new();
hasher.update(text.as_bytes());
hasher.finalize().into()
}
pub fn default_cache_path() -> PathBuf {
let root = if let Some(custom) = std::env::var_os("ZSHRS_HOME") {
PathBuf::from(custom)
} else {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join(".zshrs")
};
root.join("autoloads.rkyv")
}
pub fn cache_enabled() -> bool {
if crate::extensions::script_cache::CACHE_DISABLED.load(std::sync::atomic::Ordering::Relaxed) {
return false;
}
!matches!(
std::env::var("ZSHRS_CACHE").as_deref(),
Ok("0") | Ok("false") | Ok("no")
)
}
pub static CACHE: once_cell::sync::Lazy<Option<AutoloadCache>> = once_cell::sync::Lazy::new(|| {
if !cache_enabled() {
return None;
}
AutoloadCache::open(&default_cache_path()).ok()
});
pub fn try_load(name: &str) -> Option<Vec<u8>> {
let cache = CACHE.as_ref()?;
cache.get(name)
}
pub fn try_load_for_source(name: &str, source_dir: &str, source_sha: &[u8; 32]) -> Option<Vec<u8>> {
let cache = CACHE.as_ref()?;
cache.get_for_source(name, source_dir, source_sha)
}
pub fn try_save_one(
name: &str,
chunk_blob: &[u8],
source_dir: &str,
source_sha: [u8; 32],
) -> Result<(), String> {
let Some(cache) = CACHE.as_ref() else {
return Ok(());
};
cache.put_one(name, chunk_blob.to_vec(), source_dir, source_sha)
}
pub fn stamp_candidates(dir: &str, name: &str) -> Vec<FileStamp> {
[
format!("{dir}/{name}"),
format!("{dir}/{name}.zwc"),
format!("{dir}.zwc"),
]
.into_iter()
.map(|path| match std::fs::metadata(&path) {
Ok(m) => FileStamp {
exists: true,
mtime_ns: m.mtime() * 1_000_000_000 + m.mtime_nsec(),
len: m.len(),
path,
},
Err(_) => FileStamp {
path,
exists: false,
mtime_ns: 0,
len: 0,
},
})
.collect()
}
fn registered_digest(r: &ResolvedLoad) -> [u8; 32] {
if r.from_wordcode {
source_digest(&format!("\0zwc\0{}", r.registered))
} else {
source_digest(&r.registered)
}
}
fn stamps_fresh(stamps: &[FileStamp]) -> bool {
stamps.iter().all(|s| match std::fs::metadata(&s.path) {
Ok(m) => {
s.exists && m.mtime() * 1_000_000_000 + m.mtime_nsec() == s.mtime_ns && m.len() == s.len
}
Err(_) => !s.exists,
})
}
impl AutoloadCache {
fn resolve(&self, name: &str, fpath: &[String]) -> Option<(String, ResolvedLoad)> {
let pending_dir = self
.pending
.lock()
.iter()
.rev()
.find(|(n, ..)| n == name)
.map(|(_, _, dir, _)| dir.clone());
let (dir, resolved) = match pending_dir.and_then(|d| {
self.resolved_pending.lock().get(name).cloned().map(|r| (d, r))
}) {
Some(hit) => hit,
None => {
self.ensure_mmap();
let guard = self.mmap.lock();
let shard = guard.as_ref()?;
if !shard.header_ok() {
return None;
}
let entry = shard.lookup(name)?;
if !entry_binary_matches(entry) {
return None;
}
let resolved: ResolvedLoad =
entry.resolved.as_ref()?.deserialize(&mut rkyv::Infallible).ok()?;
(entry.source_dir.as_str().to_string(), resolved)
}
};
(fpath.iter().any(|d| *d == dir) && stamps_fresh(&resolved.stamps)).then_some((dir, resolved))
}
}
pub fn try_resolve(name: &str, fpath: &[String]) -> Option<(String, ResolvedLoad)> {
CACHE.as_ref()?.resolve(name, fpath).filter(|(_, r)| !r.registered.is_empty())
}
pub fn try_port_body(name: &str, fpath: &[String]) -> Option<(String, String)> {
let (dir, r) = CACHE.as_ref()?.resolve(name, fpath)?;
(!r.from_wordcode).then_some((dir, r.body))
}
pub fn record_port_probe(name: &str, dir: &str, body: &str) {
let Some(cache) = CACHE.as_ref() else {
return;
};
note_resolved(
name,
ResolvedLoad {
body: body.to_string(),
registered: String::new(),
dump_ksh: -1,
from_wordcode: false,
parse_failed: false,
ksh_style: false,
stamps: stamp_candidates(dir, name),
},
);
let _ = cache.put_one(name, Vec::new(), dir, [0u8; 32]);
}
pub fn try_source_dir(name: &str, fpath: &[String]) -> Option<String> {
let cache = CACHE.as_ref()?;
if let Some((dir, _)) = cache.resolve(name, fpath) {
return Some(dir);
}
cache.ensure_mmap();
let guard = cache.mmap.lock();
let shard = guard.as_ref()?;
if !shard.header_ok() {
return None;
}
let dir = shard.lookup(name)?.source_dir.as_str().to_string();
let on_fpath = fpath.iter().any(|d| *d == dir);
(on_fpath && std::path::Path::new(&format!("{dir}/{name}")).exists()).then_some(dir)
}
pub fn note_resolved(name: &str, resolved: ResolvedLoad) {
if let Some(cache) = CACHE.as_ref() {
cache.resolved_pending.lock().insert(name.to_string(), resolved);
}
}
struct StagedSearch {
dir: String,
body: String,
dump_ksh: i32,
from_wordcode: bool,
}
thread_local! {
static STAGED_SEARCH: std::cell::RefCell<HashMap<String, StagedSearch>> =
std::cell::RefCell::new(HashMap::new());
static STAGED_HIT: std::cell::RefCell<HashMap<String, ResolvedLoad>> =
std::cell::RefCell::new(HashMap::new());
}
pub fn stage_search(name: &str, dir: &str, body: &str, dump_ksh: Option<i32>, from_wordcode: bool) {
STAGED_SEARCH.with(|s| {
s.borrow_mut().insert(
name.to_string(),
StagedSearch {
dir: dir.to_string(),
body: body.to_string(),
dump_ksh: dump_ksh.unwrap_or(-1),
from_wordcode,
},
)
});
}
pub fn commit_search(name: &str, body: &str, registered: &str, parse_failed: bool, ksh_style: bool) {
let Some(s) = STAGED_SEARCH.with(|s| s.borrow_mut().remove(name)) else {
return;
};
if s.body != body {
return;
}
let stamps = stamp_candidates(&s.dir, name);
note_resolved(
name,
ResolvedLoad {
body: s.body,
registered: registered.to_string(),
dump_ksh: s.dump_ksh,
from_wordcode: s.from_wordcode,
parse_failed,
ksh_style,
stamps,
},
);
}
pub fn stage_hit(name: &str, resolved: ResolvedLoad) {
STAGED_HIT.with(|s| s.borrow_mut().insert(name.to_string(), resolved));
}
pub fn take_hit(name: &str, body: &str, ksh_style: bool) -> Option<(String, bool, bool)> {
let r = STAGED_HIT.with(|s| s.borrow_mut().remove(name))?;
(r.body == body && r.ksh_style == ksh_style).then_some((r.registered, r.from_wordcode, r.parse_failed))
}
pub fn try_put_many(entries: &[(String, Vec<u8>, String, [u8; 32])]) -> Result<(), String> {
let Some(cache) = CACHE.as_ref() else {
return Ok(());
};
cache.put_many(entries)
}
pub fn try_flush_pending() {
if let Some(cache) = CACHE.as_ref() {
if let Err(e) = cache.flush_pending() {
if !crate::atexit_teardown::active() {
tracing::warn!(error = %e, "autoload: could not flush cache");
}
}
}
}
extern "C" fn atexit_flush_pending() {
crate::atexit_teardown::mark();
let _ = std::panic::catch_unwind(try_flush_pending);
let _ = std::panic::catch_unwind(crate::deparse_cache::try_flush_pending);
let _ = std::panic::catch_unwind(crate::script_cache::try_flush_pending);
}
pub fn install_atexit_flush() {
static ONCE: std::sync::Once = std::sync::Once::new();
ONCE.call_once(|| {
unsafe {
libc::atexit(atexit_flush_pending);
}
});
}
pub fn try_remove(name: &str) {
if let Some(cache) = CACHE.as_ref() {
if let Err(e) = cache.remove(name) {
tracing::warn!(name, error = %e, "autoload: could not drop bad cache entry");
}
}
}
pub fn cached_names() -> std::collections::HashSet<String> {
CACHE.as_ref().map(|c| c.cached_names()).unwrap_or_default()
}
pub fn entry_count() -> usize {
CACHE.as_ref().map(|c| c.entry_count()).unwrap_or(0)
}
pub fn stats() -> Option<(i64, i64)> {
CACHE.as_ref().map(|c| c.stats())
}
pub fn clear() -> bool {
CACHE.as_ref().map(|c| c.clear().is_ok()).unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
const DIR: &str = "/some/fpath/dir";
#[test]
fn round_trip_one() {
let _g = crate::test_util::global_state_lock();
let dir = tempdir().unwrap();
let cache_path = dir.path().join("autoloads.rkyv");
let cache = AutoloadCache::open(&cache_path).unwrap();
cache
.put_one("foo", vec![1, 2, 3], DIR, source_digest("body"))
.unwrap();
assert_eq!(cache.get("foo"), Some(vec![1, 2, 3]));
cache.flush_pending().unwrap();
assert_eq!(cache.get("foo"), Some(vec![1, 2, 3]));
assert_eq!(cache.entry_count(), 1);
}
#[test]
fn resolution_served_by_name_until_its_inputs_change() {
let _g = crate::test_util::global_state_lock();
let tmp = tempdir().unwrap();
let fdir = tmp.path().join("fns");
std::fs::create_dir(&fdir).unwrap();
let file = fdir.join("_foo");
std::fs::write(&file, "print one\n").unwrap();
let fdir_s = fdir.to_string_lossy().to_string();
let fpath = vec!["/elsewhere".to_string(), fdir_s.clone()];
let resolved = ResolvedLoad {
body: "print one\n".to_string(),
registered: "_foo() {\nprint one\n}".to_string(),
dump_ksh: -1,
from_wordcode: false,
parse_failed: false,
ksh_style: false,
stamps: stamp_candidates(&fdir_s, "_foo"),
};
let cache_path = tmp.path().join("autoloads.rkyv");
let cache = AutoloadCache::open(&cache_path).unwrap();
cache.resolved_pending.lock().insert("_foo".to_string(), resolved.clone());
cache
.put_one("_foo", vec![1], &fdir_s, registered_digest(&resolved))
.unwrap();
cache.flush_pending().unwrap();
let fresh = AutoloadCache::open(&cache_path).unwrap();
assert_eq!(fresh.resolve("_foo", &fpath), Some((fdir_s.clone(), resolved)));
assert_eq!(fresh.resolve("_foo", &["/elsewhere".to_string()]), None, "dir left fpath");
std::fs::write(&file, "print two!\n").unwrap();
assert_eq!(fresh.resolve("_foo", &fpath), None, "file edited");
}
#[test]
fn source_text_mismatch_is_a_miss() {
let _g = crate::test_util::global_state_lock();
let dir = tempdir().unwrap();
let cache_path = dir.path().join("autoloads.rkyv");
let cache = AutoloadCache::open(&cache_path).unwrap();
let sha = source_digest("foo() {\nprint one\n}");
cache.put_one("foo", vec![1, 2, 3], DIR, sha).unwrap();
cache.flush_pending().unwrap();
assert_eq!(cache.get_for_source("foo", DIR, &sha), Some(vec![1, 2, 3]));
let edited = source_digest("foo() {\nprint two\n}");
assert!(cache.get_for_source("foo", DIR, &edited).is_none());
assert!(cache.get_for_source("foo", "/other/dir", &sha).is_none());
}
#[test]
fn an_entry_from_another_binary_is_never_served() {
let _g = crate::test_util::global_state_lock();
let dir = tempdir().unwrap();
let cache_path = dir.path().join("autoloads.rkyv");
let cache = AutoloadCache::open(&cache_path).unwrap();
let sha = source_digest("foo() {\nprint one\n}");
cache.put_one("foo", vec![1, 2, 3], DIR, sha).unwrap();
cache.flush_pending().unwrap();
let mut shard = read_owned_shard(&cache_path).expect("shard readable");
let entry = shard.entries.get_mut("foo").expect("entry present");
entry.binary_mtime_at_cache += 10_000;
write_shard_atomic(&cache_path, &shard).unwrap();
let reopened = AutoloadCache::open(&cache_path).unwrap();
assert!(
reopened.get_for_source("foo", DIR, &sha).is_none(),
"a chunk from a newer build was accepted",
);
assert!(reopened.get("foo").is_none());
}
#[test]
fn remove_drops_the_entry() {
let _g = crate::test_util::global_state_lock();
let dir = tempdir().unwrap();
let cache_path = dir.path().join("autoloads.rkyv");
let cache = AutoloadCache::open(&cache_path).unwrap();
let sha = source_digest("body");
cache.put_one("foo", vec![1], DIR, sha).unwrap();
cache.put_one("bar", vec![2], DIR, sha).unwrap();
cache.remove("foo").unwrap();
assert!(cache.get("foo").is_none());
assert_eq!(cache.get("bar"), Some(vec![2]));
}
#[test]
fn cached_names_returns_keys() {
let _g = crate::test_util::global_state_lock();
let dir = tempdir().unwrap();
let cache_path = dir.path().join("autoloads.rkyv");
let cache = AutoloadCache::open(&cache_path).unwrap();
let sha = source_digest("body");
cache.put_one("alpha", vec![1], DIR, sha).unwrap();
cache.put_one("beta", vec![2], DIR, sha).unwrap();
cache.flush_pending().unwrap();
let names = cache.cached_names();
assert!(names.contains("alpha"));
assert!(names.contains("beta"));
assert_eq!(names.len(), 2);
}
#[test]
fn corrupt_shard_returns_none() {
let _g = crate::test_util::global_state_lock();
let dir = tempdir().unwrap();
let cache_path = dir.path().join("autoloads.rkyv");
std::fs::write(&cache_path, b"garbage").unwrap();
let cache = AutoloadCache::open(&cache_path).unwrap();
assert!(cache.get("anything").is_none());
assert_eq!(cache.entry_count(), 0);
}
#[test]
fn clear_removes_file() {
let _g = crate::test_util::global_state_lock();
let dir = tempdir().unwrap();
let cache_path = dir.path().join("autoloads.rkyv");
let cache = AutoloadCache::open(&cache_path).unwrap();
cache
.put_one("x", vec![1], DIR, source_digest("body"))
.unwrap();
cache.flush_pending().unwrap();
assert!(cache_path.exists());
cache.clear().unwrap();
assert!(!cache_path.exists());
}
#[test]
fn a_write_leaves_no_temp_file_behind() {
let _g = crate::test_util::global_state_lock();
let dir = tempdir().unwrap();
let cache_path = dir.path().join("autoloads.rkyv");
let cache = AutoloadCache::open(&cache_path).unwrap();
cache
.put_one("x", vec![1], DIR, source_digest("body"))
.unwrap();
cache.flush_pending().unwrap();
let temps: Vec<_> = std::fs::read_dir(dir.path())
.unwrap()
.flatten()
.map(|e| e.file_name().into_string().unwrap())
.filter(|n| n.contains(".tmp."))
.collect();
assert!(temps.is_empty(), "temp files left behind: {temps:?}");
}
#[test]
fn put_one_defers_the_shard_write_until_flush() {
let _g = crate::test_util::global_state_lock();
let dir = tempdir().unwrap();
let cache_path = dir.path().join("autoloads.rkyv");
let cache = AutoloadCache::open(&cache_path).unwrap();
let sha = source_digest("body");
for i in 0..8 {
cache.put_one(&format!("f{i}"), vec![i], DIR, sha).unwrap();
}
assert!(
!cache_path.exists(),
"put_one wrote the shard before the flush boundary"
);
assert_eq!(cache.get_for_source("f3", DIR, &sha), Some(vec![3]));
cache.flush_pending().unwrap();
assert_eq!(cache.entry_count(), 8);
assert_eq!(cache.get_for_source("f7", DIR, &sha), Some(vec![7]));
}
#[test]
fn flush_preserves_entries_written_by_another_process() {
let _g = crate::test_util::global_state_lock();
let dir = tempdir().unwrap();
let cache_path = dir.path().join("autoloads.rkyv");
let sha = source_digest("body");
let peer = AutoloadCache::open(&cache_path).unwrap();
peer.put_one("peer_fn", vec![9], DIR, sha).unwrap();
peer.flush_pending().unwrap();
let mine = AutoloadCache::open(&cache_path).unwrap();
mine.put_one("my_fn", vec![1], DIR, sha).unwrap();
mine.flush_pending().unwrap();
assert_eq!(mine.get_for_source("peer_fn", DIR, &sha), Some(vec![9]));
assert_eq!(mine.get_for_source("my_fn", DIR, &sha), Some(vec![1]));
}
#[test]
fn remove_drops_a_buffered_entry_before_it_reaches_disk() {
let _g = crate::test_util::global_state_lock();
let dir = tempdir().unwrap();
let cache_path = dir.path().join("autoloads.rkyv");
let cache = AutoloadCache::open(&cache_path).unwrap();
let sha = source_digest("body");
cache.put_one("bad", vec![1], DIR, sha).unwrap();
cache.remove("bad").unwrap();
cache.flush_pending().unwrap();
assert!(cache.get("bad").is_none());
let reopened = AutoloadCache::open(&cache_path).unwrap();
assert!(reopened.get("bad").is_none());
}
#[test]
fn flush_with_the_lock_held_elsewhere_returns_and_keeps_the_batch() {
let _g = crate::test_util::global_state_lock();
let dir = tempdir().unwrap();
let cache_path = dir.path().join("autoloads.rkyv");
let cache = std::sync::Arc::new(AutoloadCache::open(&cache_path).unwrap());
let sha = source_digest("body");
cache.put_one("f", vec![1], DIR, sha).unwrap();
let held = acquire_lock(&cache.lock_path).unwrap();
let (tx, rx) = std::sync::mpsc::channel();
let flusher = std::sync::Arc::clone(&cache);
std::thread::spawn(move || {
let _ = tx.send(flusher.flush_pending());
});
let res = rx
.recv_timeout(std::time::Duration::from_secs(5))
.expect("flush_pending blocked on a lock held elsewhere");
assert!(res.is_ok());
assert!(!cache_path.exists(), "wrote the shard without the lock");
assert_eq!(cache.get_for_source("f", DIR, &sha), Some(vec![1]));
drop(held);
cache.flush_pending().unwrap();
assert!(cache_path.exists());
assert_eq!(cache.entry_count(), 1);
}
}