1use std::collections::HashMap;
57use std::fs::File;
58use std::path::{Path, PathBuf};
59use std::sync::OnceLock;
60use std::time::{SystemTime, UNIX_EPOCH};
61
62use memmap2::Mmap;
63use parking_lot::Mutex;
64use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
65use std::os::unix::fs::MetadataExt;
66
67pub const SHARD_MAGIC: u32 = 0x5A52414C;
69pub const SHARD_FORMAT_VERSION: u32 = 6;
82#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone)]
84#[archive(check_bytes)]
85pub struct ShardHeader {
86 pub magic: u32,
88 pub format_version: u32,
90 pub zshrs_version: String,
92 pub pointer_width: u32,
94 pub built_at_secs: u64,
96}
97#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone)]
99#[archive(check_bytes)]
100pub struct AutoloadEntry {
101 pub binary_mtime_at_cache: i64,
103 pub binary_len_at_cache: u64,
107 pub cached_at_secs: i64,
109 pub source_dir: String,
113 pub source_sha: [u8; 32],
118 pub chunk_blob: Vec<u8>,
120 pub resolved: Option<ResolvedLoad>,
125}
126
127#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone, PartialEq)]
131#[archive(check_bytes)]
132pub struct ResolvedLoad {
133 pub body: String,
135 pub registered: String,
138 pub dump_ksh: i32,
140 pub from_wordcode: bool,
142 pub parse_failed: bool,
144 pub ksh_style: bool,
147 pub stamps: Vec<FileStamp>,
150}
151
152#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone, PartialEq)]
154#[archive(check_bytes)]
155pub struct FileStamp {
156 pub path: String,
158 pub exists: bool,
160 pub mtime_ns: i64,
162 pub len: u64,
164}
165#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone)]
167#[archive(check_bytes)]
168pub struct AutoloadShard {
169 pub header: ShardHeader,
171 pub entries: HashMap<String, AutoloadEntry>,
173}
174pub struct MmappedShard {
176 _mmap: Mmap,
178 archived: *const ArchivedAutoloadShard,
180}
181
182unsafe impl Send for MmappedShard {}
183unsafe impl Sync for MmappedShard {}
184
185impl MmappedShard {
186 pub fn open(path: &Path) -> Option<Self> {
188 let file = File::open(path).ok()?;
189 let mmap = unsafe { Mmap::map(&file).ok()? };
190 let archived = rkyv::check_archived_root::<AutoloadShard>(&mmap[..]).ok()?;
191 let archived_ptr = archived as *const ArchivedAutoloadShard;
192 Some(Self {
193 _mmap: mmap,
194 archived: archived_ptr,
195 })
196 }
197
198 fn shard(&self) -> &ArchivedAutoloadShard {
199 unsafe { &*self.archived }
200 }
201
202 fn header_ok(&self) -> bool {
203 let h = &self.shard().header;
204 let magic: u32 = h.magic.into();
205 let fv: u32 = h.format_version.into();
206 let pw: u32 = h.pointer_width.into();
207 magic == SHARD_MAGIC
208 && fv == SHARD_FORMAT_VERSION
209 && pw as usize == std::mem::size_of::<usize>()
210 && h.zshrs_version.as_str() == env!("CARGO_PKG_VERSION")
211 }
212
213 fn lookup(&self, name: &str) -> Option<&ArchivedAutoloadEntry> {
214 self.shard().entries.get(name)
215 }
216}
217
218fn entry_binary_matches(entry: &ArchivedAutoloadEntry) -> bool {
225 let Some((mtime, len)) = current_binary_identity() else {
226 return false;
228 };
229 let cached_mtime: i64 = entry.binary_mtime_at_cache.into();
230 let cached_len: u64 = entry.binary_len_at_cache.into();
231 cached_mtime == mtime && cached_len == len
232}
233
234pub struct AutoloadCache {
236 path: PathBuf,
238 lock_path: PathBuf,
240 mmap: Mutex<Option<MmappedShard>>,
242 pending: Mutex<Vec<(String, Vec<u8>, String, [u8; 32])>>,
252 resolved_pending: Mutex<HashMap<String, ResolvedLoad>>,
255}
256
257const PENDING_FLUSH_MAX: usize = 256;
264
265impl AutoloadCache {
266 pub fn open(path: &Path) -> std::io::Result<Self> {
268 if let Some(parent) = path.parent() {
269 std::fs::create_dir_all(parent)?;
270 }
271 let parent = path.parent().unwrap_or_else(|| Path::new("/tmp"));
272 let lock_path = parent.join(format!(
273 "{}.lock",
274 path.file_name()
275 .and_then(|s| s.to_str())
276 .unwrap_or("autoloads.rkyv")
277 ));
278 Ok(Self {
279 path: path.to_path_buf(),
280 lock_path,
281 mmap: Mutex::new(None),
282 pending: Mutex::new(Vec::new()),
283 resolved_pending: Mutex::new(HashMap::new()),
284 })
285 }
286
287 fn ensure_mmap(&self) {
288 let mut guard = self.mmap.lock();
289 if guard.is_none() {
290 *guard = MmappedShard::open(&self.path);
291 }
292 }
293
294 fn invalidate_mmap(&self) {
295 let mut guard = self.mmap.lock();
296 *guard = None;
297 }
298 pub fn get(&self, name: &str) -> Option<Vec<u8>> {
304 if let Some(blob) = self.pending_lookup(name, None, None) {
307 return (!blob.is_empty()).then_some(blob);
308 }
309 self.ensure_mmap();
310 let guard = self.mmap.lock();
311 let shard = guard.as_ref()?;
312 if !shard.header_ok() {
313 return None;
314 }
315 let entry = shard.lookup(name)?;
316 if !entry_binary_matches(entry) {
317 return None;
318 }
319 (!entry.chunk_blob.is_empty()).then(|| entry.chunk_blob.as_slice().to_vec())
320 }
321
322 pub fn get_for_source(
328 &self,
329 name: &str,
330 source_dir: &str,
331 source_sha: &[u8; 32],
332 ) -> Option<Vec<u8>> {
333 if let Some(blob) = self.pending_lookup(name, Some(source_dir), Some(source_sha)) {
334 return Some(blob);
335 }
336 self.ensure_mmap();
337 let guard = self.mmap.lock();
338 let shard = guard.as_ref()?;
339 if !shard.header_ok() {
340 return None;
341 }
342 let entry = shard.lookup(name)?;
343 if !entry_binary_matches(entry) {
344 return None;
345 }
346 if entry.source_dir.as_str() != source_dir {
347 return None;
348 }
349 if entry.source_sha.as_slice() != source_sha.as_slice() {
350 return None;
351 }
352 Some(entry.chunk_blob.as_slice().to_vec())
353 }
354
355 fn owned_shard_for_write(&self) -> AutoloadShard {
358 match read_owned_shard(&self.path) {
359 Some(s)
360 if s.header.zshrs_version == env!("CARGO_PKG_VERSION")
361 && s.header.pointer_width as usize == std::mem::size_of::<usize>()
362 && s.header.format_version == SHARD_FORMAT_VERSION =>
363 {
364 s
365 }
366 _ => fresh_shard(),
367 }
368 }
369
370 pub fn put_one(
379 &self,
380 name: &str,
381 chunk_blob: Vec<u8>,
382 source_dir: &str,
383 source_sha: [u8; 32],
384 ) -> Result<(), String> {
385 {
386 let mut pending = self.pending.lock();
387 pending.push((
388 name.to_string(),
389 chunk_blob,
390 source_dir.to_string(),
391 source_sha,
392 ));
393 if pending.len() < PENDING_FLUSH_MAX {
394 return Ok(());
395 }
396 }
397 self.flush_pending()
398 }
399
400 pub fn flush_pending(&self) -> Result<(), String> {
415 let batch = {
416 let mut pending = self.pending.lock();
417 if pending.is_empty() && self.resolved_pending.lock().is_empty() {
420 return Ok(());
421 }
422 std::mem::take(&mut *pending)
423 };
424 let Some(lock) = try_acquire_lock(&self.lock_path) else {
425 self.requeue(batch);
426 return Ok(());
427 };
428 self.put_many_locked(&batch, lock)
429 }
430
431 fn requeue(&self, mut batch: Vec<(String, Vec<u8>, String, [u8; 32])>) {
436 let mut pending = self.pending.lock();
437 batch.append(&mut pending);
438 let excess = batch.len().saturating_sub(PENDING_FLUSH_MAX);
439 batch.drain(..excess);
440 *pending = batch;
441 }
442
443 fn pending_lookup(
450 &self,
451 name: &str,
452 source_dir: Option<&str>,
453 source_sha: Option<&[u8; 32]>,
454 ) -> Option<Vec<u8>> {
455 let pending = self.pending.lock();
456 pending.iter().rev().find_map(|(n, blob, dir, sha)| {
459 if n != name {
460 return None;
461 }
462 if source_dir.is_some_and(|d| d != dir) {
463 return None;
464 }
465 if source_sha.is_some_and(|s| s != sha) {
466 return None;
467 }
468 Some(blob.clone())
469 })
470 }
471
472 pub fn put_many(&self, entries: &[(String, Vec<u8>, String, [u8; 32])]) -> Result<(), String> {
479 if entries.is_empty() && self.resolved_pending.lock().is_empty() {
480 return Ok(());
481 }
482 let Some(lock) = acquire_lock(&self.lock_path) else {
483 return Ok(());
484 };
485 self.put_many_locked(entries, lock)
486 }
487
488 fn put_many_locked(
491 &self,
492 entries: &[(String, Vec<u8>, String, [u8; 32])],
493 _lock: nix::fcntl::Flock<File>,
494 ) -> Result<(), String> {
495 let mut shard = self.owned_shard_for_write();
496 let (bin_mtime, bin_len) = current_binary_identity().unwrap_or((0, 0));
497 let now = now_secs();
498 for (name, chunk_blob, source_dir, source_sha) in entries {
499 if chunk_blob.is_empty() && shard.entries.contains_key(name) {
505 continue;
506 }
507 let noted = self
508 .resolved_pending
509 .lock()
510 .remove(name)
511 .filter(|r| r.registered.is_empty() || registered_digest(r) == *source_sha);
512 let prior = shard
513 .entries
514 .get(name)
515 .filter(|e| e.source_dir == *source_dir && e.source_sha == *source_sha)
516 .and_then(|e| e.resolved.clone());
517 shard.entries.insert(
518 name.clone(),
519 AutoloadEntry {
520 binary_mtime_at_cache: bin_mtime,
521 binary_len_at_cache: bin_len,
522 cached_at_secs: now,
523 source_dir: source_dir.clone(),
524 source_sha: *source_sha,
525 chunk_blob: chunk_blob.clone(),
526 resolved: noted.or(prior),
527 },
528 );
529 }
530 for (name, r) in std::mem::take(&mut *self.resolved_pending.lock()) {
534 if let Some(e) = shard.entries.get_mut(&name) {
535 let same_dir = r
536 .stamps
537 .first()
538 .is_some_and(|s| s.path == format!("{}/{}", e.source_dir, name));
539 let fits = if r.registered.is_empty() {
542 e.resolved.is_none()
543 } else {
544 registered_digest(&r) == e.source_sha
545 };
546 if same_dir && fits {
547 e.resolved = Some(r);
548 }
549 }
550 }
551 shard.header.built_at_secs = now as u64;
552 write_shard_atomic(&self.path, &shard)?;
553 self.invalidate_mmap();
554 Ok(())
555 }
556
557 pub fn remove(&self, name: &str) -> Result<(), String> {
564 self.pending.lock().retain(|(n, _, _, _)| n != name);
567 self.resolved_pending.lock().remove(name);
568 let _lock = match acquire_lock(&self.lock_path) {
569 Some(l) => l,
570 None => return Ok(()),
571 };
572 let mut shard = self.owned_shard_for_write();
573 if shard.entries.remove(name).is_none() {
574 return Ok(());
575 }
576 shard.header.built_at_secs = now_secs() as u64;
577 write_shard_atomic(&self.path, &shard)?;
578 self.invalidate_mmap();
579 Ok(())
580 }
581 pub fn entry_count(&self) -> usize {
583 self.ensure_mmap();
584 let guard = self.mmap.lock();
585 guard.as_ref().map(|s| s.shard().entries.len()).unwrap_or(0)
586 }
587
588 pub fn cached_names(&self) -> std::collections::HashSet<String> {
592 self.ensure_mmap();
593 let guard = self.mmap.lock();
594 let Some(shard) = guard.as_ref() else {
595 return std::collections::HashSet::new();
596 };
597 shard
598 .shard()
599 .entries
600 .keys()
601 .map(|k| k.as_str().to_string())
602 .collect()
603 }
604 pub fn stats(&self) -> (i64, i64) {
606 self.ensure_mmap();
607 let guard = self.mmap.lock();
608 let Some(shard) = guard.as_ref() else {
609 return (0, 0);
610 };
611 let count = shard.shard().entries.len() as i64;
612 let bytes: i64 = shard
613 .shard()
614 .entries
615 .values()
616 .map(|e| e.chunk_blob.len() as i64)
617 .sum();
618 (count, bytes)
619 }
620 pub fn clear(&self) -> std::io::Result<()> {
622 let _lock = acquire_lock(&self.lock_path);
623 let res = match std::fs::remove_file(&self.path) {
624 Ok(()) => Ok(()),
625 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
626 Err(e) => Err(e),
627 };
628 crate::atomic_write::reap_orphan_temps(&self.path);
629 self.invalidate_mmap();
630 res
631 }
632}
633
634fn acquire_lock(path: &Path) -> Option<nix::fcntl::Flock<File>> {
635 let f = File::options()
636 .read(true)
637 .write(true)
638 .create(true)
639 .truncate(false)
640 .open(path)
641 .ok()?;
642 nix::fcntl::Flock::lock(f, nix::fcntl::FlockArg::LockExclusive).ok()
643}
644
645fn try_acquire_lock(path: &Path) -> Option<nix::fcntl::Flock<File>> {
648 let f = File::options()
649 .read(true)
650 .write(true)
651 .create(true)
652 .truncate(false)
653 .open(path)
654 .ok()?;
655 nix::fcntl::Flock::lock(f, nix::fcntl::FlockArg::LockExclusiveNonblock).ok()
656}
657
658fn fresh_shard() -> AutoloadShard {
659 AutoloadShard {
660 header: ShardHeader {
661 magic: SHARD_MAGIC,
662 format_version: SHARD_FORMAT_VERSION,
663 zshrs_version: env!("CARGO_PKG_VERSION").to_string(),
664 pointer_width: std::mem::size_of::<usize>() as u32,
665 built_at_secs: now_secs() as u64,
666 },
667 entries: HashMap::new(),
668 }
669}
670
671fn read_owned_shard(path: &Path) -> Option<AutoloadShard> {
672 let bytes = std::fs::read(path).ok()?;
673 let archived = rkyv::check_archived_root::<AutoloadShard>(&bytes[..]).ok()?;
674 archived.deserialize(&mut rkyv::Infallible).ok()
675}
676
677fn write_shard_atomic(path: &Path, shard: &AutoloadShard) -> Result<(), String> {
678 let bytes = rkyv::to_bytes::<_, 4096>(shard).map_err(|e| format!("rkyv serialize: {}", e))?;
679 crate::atomic_write::write_bytes_atomic(path, &bytes)
680}
681
682fn now_secs() -> i64 {
683 SystemTime::now()
684 .duration_since(UNIX_EPOCH)
685 .map(|d| d.as_secs() as i64)
686 .unwrap_or(0)
687}
688
689fn current_binary_identity() -> Option<(i64, u64)> {
693 static BIN_ID: OnceLock<Option<(i64, u64)>> = OnceLock::new();
694 *BIN_ID.get_or_init(|| {
695 let exe = std::env::current_exe().ok()?;
696 let meta = std::fs::metadata(&exe).ok()?;
697 Some((meta.mtime(), meta.len()))
698 })
699}
700
701pub fn source_digest(text: &str) -> [u8; 32] {
708 use sha2::Digest;
709 let mut hasher = sha2::Sha256::new();
710 hasher.update(text.as_bytes());
711 hasher.finalize().into()
712}
713pub fn default_cache_path() -> PathBuf {
721 let root = if let Some(custom) = std::env::var_os("ZSHRS_HOME") {
722 PathBuf::from(custom)
723 } else {
724 dirs::home_dir()
725 .unwrap_or_else(|| PathBuf::from("/tmp"))
726 .join(".zshrs")
727 };
728 root.join("autoloads.rkyv")
729}
730pub fn cache_enabled() -> bool {
735 if crate::extensions::script_cache::CACHE_DISABLED.load(std::sync::atomic::Ordering::Relaxed) {
736 return false;
737 }
738 !matches!(
739 std::env::var("ZSHRS_CACHE").as_deref(),
740 Ok("0") | Ok("false") | Ok("no")
741 )
742}
743pub static CACHE: once_cell::sync::Lazy<Option<AutoloadCache>> = once_cell::sync::Lazy::new(|| {
745 if !cache_enabled() {
746 return None;
747 }
748 AutoloadCache::open(&default_cache_path()).ok()
749});
750pub fn try_load(name: &str) -> Option<Vec<u8>> {
753 let cache = CACHE.as_ref()?;
754 cache.get(name)
755}
756
757pub fn try_load_for_source(name: &str, source_dir: &str, source_sha: &[u8; 32]) -> Option<Vec<u8>> {
760 let cache = CACHE.as_ref()?;
761 cache.get_for_source(name, source_dir, source_sha)
762}
763
764pub fn try_save_one(
766 name: &str,
767 chunk_blob: &[u8],
768 source_dir: &str,
769 source_sha: [u8; 32],
770) -> Result<(), String> {
771 let Some(cache) = CACHE.as_ref() else {
772 return Ok(());
773 };
774 cache.put_one(name, chunk_blob.to_vec(), source_dir, source_sha)
775}
776
777pub fn stamp_candidates(dir: &str, name: &str) -> Vec<FileStamp> {
782 [
783 format!("{dir}/{name}"),
784 format!("{dir}/{name}.zwc"),
785 format!("{dir}.zwc"),
786 ]
787 .into_iter()
788 .map(|path| match std::fs::metadata(&path) {
789 Ok(m) => FileStamp {
790 exists: true,
791 mtime_ns: m.mtime() * 1_000_000_000 + m.mtime_nsec(),
792 len: m.len(),
793 path,
794 },
795 Err(_) => FileStamp {
796 path,
797 exists: false,
798 mtime_ns: 0,
799 len: 0,
800 },
801 })
802 .collect()
803}
804
805fn registered_digest(r: &ResolvedLoad) -> [u8; 32] {
809 if r.from_wordcode {
810 source_digest(&format!("\0zwc\0{}", r.registered))
811 } else {
812 source_digest(&r.registered)
813 }
814}
815
816fn stamps_fresh(stamps: &[FileStamp]) -> bool {
817 stamps.iter().all(|s| match std::fs::metadata(&s.path) {
818 Ok(m) => {
819 s.exists && m.mtime() * 1_000_000_000 + m.mtime_nsec() == s.mtime_ns && m.len() == s.len
820 }
821 Err(_) => !s.exists,
822 })
823}
824
825impl AutoloadCache {
826 fn resolve(&self, name: &str, fpath: &[String]) -> Option<(String, ResolvedLoad)> {
829 let pending_dir = self
830 .pending
831 .lock()
832 .iter()
833 .rev()
834 .find(|(n, ..)| n == name)
835 .map(|(_, _, dir, _)| dir.clone());
836 let (dir, resolved) = match pending_dir.and_then(|d| {
837 self.resolved_pending.lock().get(name).cloned().map(|r| (d, r))
838 }) {
839 Some(hit) => hit,
840 None => {
841 self.ensure_mmap();
842 let guard = self.mmap.lock();
843 let shard = guard.as_ref()?;
844 if !shard.header_ok() {
845 return None;
846 }
847 let entry = shard.lookup(name)?;
848 if !entry_binary_matches(entry) {
849 return None;
850 }
851 let resolved: ResolvedLoad =
852 entry.resolved.as_ref()?.deserialize(&mut rkyv::Infallible).ok()?;
853 (entry.source_dir.as_str().to_string(), resolved)
854 }
855 };
856 (fpath.iter().any(|d| *d == dir) && stamps_fresh(&resolved.stamps)).then_some((dir, resolved))
857 }
858}
859
860pub fn try_resolve(name: &str, fpath: &[String]) -> Option<(String, ResolvedLoad)> {
872 CACHE.as_ref()?.resolve(name, fpath).filter(|(_, r)| !r.registered.is_empty())
874}
875
876pub fn try_port_body(name: &str, fpath: &[String]) -> Option<(String, String)> {
881 let (dir, r) = CACHE.as_ref()?.resolve(name, fpath)?;
882 (!r.from_wordcode).then_some((dir, r.body))
883}
884
885pub fn record_port_probe(name: &str, dir: &str, body: &str) {
890 let Some(cache) = CACHE.as_ref() else {
891 return;
892 };
893 note_resolved(
894 name,
895 ResolvedLoad {
896 body: body.to_string(),
897 registered: String::new(),
898 dump_ksh: -1,
899 from_wordcode: false,
900 parse_failed: false,
901 ksh_style: false,
902 stamps: stamp_candidates(dir, name),
903 },
904 );
905 let _ = cache.put_one(name, Vec::new(), dir, [0u8; 32]);
906}
907
908pub fn try_source_dir(name: &str, fpath: &[String]) -> Option<String> {
913 let cache = CACHE.as_ref()?;
914 if let Some((dir, _)) = cache.resolve(name, fpath) {
915 return Some(dir);
916 }
917 cache.ensure_mmap();
918 let guard = cache.mmap.lock();
919 let shard = guard.as_ref()?;
920 if !shard.header_ok() {
921 return None;
922 }
923 let dir = shard.lookup(name)?.source_dir.as_str().to_string();
924 let on_fpath = fpath.iter().any(|d| *d == dir);
925 (on_fpath && std::path::Path::new(&format!("{dir}/{name}")).exists()).then_some(dir)
926}
927
928pub fn note_resolved(name: &str, resolved: ResolvedLoad) {
931 if let Some(cache) = CACHE.as_ref() {
932 cache.resolved_pending.lock().insert(name.to_string(), resolved);
933 }
934}
935
936struct StagedSearch {
939 dir: String,
940 body: String,
941 dump_ksh: i32,
942 from_wordcode: bool,
943}
944
945thread_local! {
946 static STAGED_SEARCH: std::cell::RefCell<HashMap<String, StagedSearch>> =
947 std::cell::RefCell::new(HashMap::new());
948 static STAGED_HIT: std::cell::RefCell<HashMap<String, ResolvedLoad>> =
949 std::cell::RefCell::new(HashMap::new());
950}
951
952pub fn stage_search(name: &str, dir: &str, body: &str, dump_ksh: Option<i32>, from_wordcode: bool) {
954 STAGED_SEARCH.with(|s| {
955 s.borrow_mut().insert(
956 name.to_string(),
957 StagedSearch {
958 dir: dir.to_string(),
959 body: body.to_string(),
960 dump_ksh: dump_ksh.unwrap_or(-1),
961 from_wordcode,
962 },
963 )
964 });
965}
966
967pub fn commit_search(name: &str, body: &str, registered: &str, parse_failed: bool, ksh_style: bool) {
970 let Some(s) = STAGED_SEARCH.with(|s| s.borrow_mut().remove(name)) else {
971 return;
972 };
973 if s.body != body {
974 return;
975 }
976 let stamps = stamp_candidates(&s.dir, name);
977 note_resolved(
978 name,
979 ResolvedLoad {
980 body: s.body,
981 registered: registered.to_string(),
982 dump_ksh: s.dump_ksh,
983 from_wordcode: s.from_wordcode,
984 parse_failed,
985 ksh_style,
986 stamps,
987 },
988 );
989}
990
991pub fn stage_hit(name: &str, resolved: ResolvedLoad) {
993 STAGED_HIT.with(|s| s.borrow_mut().insert(name.to_string(), resolved));
994}
995
996pub fn take_hit(name: &str, body: &str, ksh_style: bool) -> Option<(String, bool, bool)> {
1000 let r = STAGED_HIT.with(|s| s.borrow_mut().remove(name))?;
1001 (r.body == body && r.ksh_style == ksh_style).then_some((r.registered, r.from_wordcode, r.parse_failed))
1002}
1003
1004pub fn try_put_many(entries: &[(String, Vec<u8>, String, [u8; 32])]) -> Result<(), String> {
1007 let Some(cache) = CACHE.as_ref() else {
1008 return Ok(());
1009 };
1010 cache.put_many(entries)
1011}
1012
1013pub fn try_flush_pending() {
1019 if let Some(cache) = CACHE.as_ref() {
1020 if let Err(e) = cache.flush_pending() {
1021 if !crate::atexit_teardown::active() {
1025 tracing::warn!(error = %e, "autoload: could not flush cache");
1026 }
1027 }
1028 }
1029}
1030
1031extern "C" fn atexit_flush_pending() {
1054 crate::atexit_teardown::mark();
1055 let _ = std::panic::catch_unwind(try_flush_pending);
1056 let _ = std::panic::catch_unwind(crate::deparse_cache::try_flush_pending);
1058}
1059
1060pub fn install_atexit_flush() {
1062 static ONCE: std::sync::Once = std::sync::Once::new();
1063 ONCE.call_once(|| {
1064 unsafe {
1067 libc::atexit(atexit_flush_pending);
1068 }
1069 });
1070}
1071
1072pub fn try_remove(name: &str) {
1074 if let Some(cache) = CACHE.as_ref() {
1075 if let Err(e) = cache.remove(name) {
1076 tracing::warn!(name, error = %e, "autoload: could not drop bad cache entry");
1077 }
1078 }
1079}
1080
1081pub fn cached_names() -> std::collections::HashSet<String> {
1083 CACHE.as_ref().map(|c| c.cached_names()).unwrap_or_default()
1084}
1085pub fn entry_count() -> usize {
1087 CACHE.as_ref().map(|c| c.entry_count()).unwrap_or(0)
1088}
1089pub fn stats() -> Option<(i64, i64)> {
1091 CACHE.as_ref().map(|c| c.stats())
1092}
1093pub fn clear() -> bool {
1095 CACHE.as_ref().map(|c| c.clear().is_ok()).unwrap_or(false)
1096}
1097
1098#[cfg(test)]
1099mod tests {
1100 use super::*;
1101 use tempfile::tempdir;
1102
1103 const DIR: &str = "/some/fpath/dir";
1104
1105 #[test]
1106 fn round_trip_one() {
1107 let _g = crate::test_util::global_state_lock();
1108 let dir = tempdir().unwrap();
1109 let cache_path = dir.path().join("autoloads.rkyv");
1110 let cache = AutoloadCache::open(&cache_path).unwrap();
1111 cache
1112 .put_one("foo", vec![1, 2, 3], DIR, source_digest("body"))
1113 .unwrap();
1114 assert_eq!(cache.get("foo"), Some(vec![1, 2, 3]));
1116 cache.flush_pending().unwrap();
1117 assert_eq!(cache.get("foo"), Some(vec![1, 2, 3]));
1118 assert_eq!(cache.entry_count(), 1);
1119 }
1120
1121 #[test]
1125 fn resolution_served_by_name_until_its_inputs_change() {
1126 let _g = crate::test_util::global_state_lock();
1127 let tmp = tempdir().unwrap();
1128 let fdir = tmp.path().join("fns");
1129 std::fs::create_dir(&fdir).unwrap();
1130 let file = fdir.join("_foo");
1131 std::fs::write(&file, "print one\n").unwrap();
1132 let fdir_s = fdir.to_string_lossy().to_string();
1133 let fpath = vec!["/elsewhere".to_string(), fdir_s.clone()];
1134 let resolved = ResolvedLoad {
1135 body: "print one\n".to_string(),
1136 registered: "_foo() {\nprint one\n}".to_string(),
1137 dump_ksh: -1,
1138 from_wordcode: false,
1139 parse_failed: false,
1140 ksh_style: false,
1141 stamps: stamp_candidates(&fdir_s, "_foo"),
1142 };
1143 let cache_path = tmp.path().join("autoloads.rkyv");
1144 let cache = AutoloadCache::open(&cache_path).unwrap();
1145 cache.resolved_pending.lock().insert("_foo".to_string(), resolved.clone());
1146 cache
1147 .put_one("_foo", vec![1], &fdir_s, registered_digest(&resolved))
1148 .unwrap();
1149 cache.flush_pending().unwrap();
1150
1151 let fresh = AutoloadCache::open(&cache_path).unwrap();
1152 assert_eq!(fresh.resolve("_foo", &fpath), Some((fdir_s.clone(), resolved)));
1153 assert_eq!(fresh.resolve("_foo", &["/elsewhere".to_string()]), None, "dir left fpath");
1154 std::fs::write(&file, "print two!\n").unwrap();
1155 assert_eq!(fresh.resolve("_foo", &fpath), None, "file edited");
1156 }
1157
1158 #[test]
1159 fn source_text_mismatch_is_a_miss() {
1160 let _g = crate::test_util::global_state_lock();
1164 let dir = tempdir().unwrap();
1165 let cache_path = dir.path().join("autoloads.rkyv");
1166 let cache = AutoloadCache::open(&cache_path).unwrap();
1167 let sha = source_digest("foo() {\nprint one\n}");
1168 cache.put_one("foo", vec![1, 2, 3], DIR, sha).unwrap();
1169 cache.flush_pending().unwrap();
1170 assert_eq!(cache.get_for_source("foo", DIR, &sha), Some(vec![1, 2, 3]));
1171 let edited = source_digest("foo() {\nprint two\n}");
1173 assert!(cache.get_for_source("foo", DIR, &edited).is_none());
1174 assert!(cache.get_for_source("foo", "/other/dir", &sha).is_none());
1176 }
1177
1178 #[test]
1179 fn an_entry_from_another_binary_is_never_served() {
1180 let _g = crate::test_util::global_state_lock();
1185 let dir = tempdir().unwrap();
1186 let cache_path = dir.path().join("autoloads.rkyv");
1187 let cache = AutoloadCache::open(&cache_path).unwrap();
1188 let sha = source_digest("foo() {\nprint one\n}");
1189 cache.put_one("foo", vec![1, 2, 3], DIR, sha).unwrap();
1190 cache.flush_pending().unwrap();
1191
1192 let mut shard = read_owned_shard(&cache_path).expect("shard readable");
1194 let entry = shard.entries.get_mut("foo").expect("entry present");
1195 entry.binary_mtime_at_cache += 10_000;
1196 write_shard_atomic(&cache_path, &shard).unwrap();
1197
1198 let reopened = AutoloadCache::open(&cache_path).unwrap();
1199 assert!(
1200 reopened.get_for_source("foo", DIR, &sha).is_none(),
1201 "a chunk from a newer build was accepted",
1202 );
1203 assert!(reopened.get("foo").is_none());
1204 }
1205
1206 #[test]
1207 fn remove_drops_the_entry() {
1208 let _g = crate::test_util::global_state_lock();
1209 let dir = tempdir().unwrap();
1210 let cache_path = dir.path().join("autoloads.rkyv");
1211 let cache = AutoloadCache::open(&cache_path).unwrap();
1212 let sha = source_digest("body");
1213 cache.put_one("foo", vec![1], DIR, sha).unwrap();
1214 cache.put_one("bar", vec![2], DIR, sha).unwrap();
1215 cache.remove("foo").unwrap();
1216 assert!(cache.get("foo").is_none());
1217 assert_eq!(cache.get("bar"), Some(vec![2]));
1218 }
1219
1220 #[test]
1221 fn cached_names_returns_keys() {
1222 let _g = crate::test_util::global_state_lock();
1223 let dir = tempdir().unwrap();
1224 let cache_path = dir.path().join("autoloads.rkyv");
1225 let cache = AutoloadCache::open(&cache_path).unwrap();
1226 let sha = source_digest("body");
1227 cache.put_one("alpha", vec![1], DIR, sha).unwrap();
1228 cache.put_one("beta", vec![2], DIR, sha).unwrap();
1229 cache.flush_pending().unwrap();
1230 let names = cache.cached_names();
1231 assert!(names.contains("alpha"));
1232 assert!(names.contains("beta"));
1233 assert_eq!(names.len(), 2);
1234 }
1235
1236 #[test]
1237 fn corrupt_shard_returns_none() {
1238 let _g = crate::test_util::global_state_lock();
1239 let dir = tempdir().unwrap();
1240 let cache_path = dir.path().join("autoloads.rkyv");
1241 std::fs::write(&cache_path, b"garbage").unwrap();
1242 let cache = AutoloadCache::open(&cache_path).unwrap();
1243 assert!(cache.get("anything").is_none());
1244 assert_eq!(cache.entry_count(), 0);
1245 }
1246
1247 #[test]
1248 fn clear_removes_file() {
1249 let _g = crate::test_util::global_state_lock();
1250 let dir = tempdir().unwrap();
1251 let cache_path = dir.path().join("autoloads.rkyv");
1252 let cache = AutoloadCache::open(&cache_path).unwrap();
1253 cache
1254 .put_one("x", vec![1], DIR, source_digest("body"))
1255 .unwrap();
1256 cache.flush_pending().unwrap();
1257 assert!(cache_path.exists());
1258 cache.clear().unwrap();
1259 assert!(!cache_path.exists());
1260 }
1261
1262 #[test]
1263 fn a_write_leaves_no_temp_file_behind() {
1264 let _g = crate::test_util::global_state_lock();
1267 let dir = tempdir().unwrap();
1268 let cache_path = dir.path().join("autoloads.rkyv");
1269 let cache = AutoloadCache::open(&cache_path).unwrap();
1270 cache
1271 .put_one("x", vec![1], DIR, source_digest("body"))
1272 .unwrap();
1273 cache.flush_pending().unwrap();
1275 let temps: Vec<_> = std::fs::read_dir(dir.path())
1276 .unwrap()
1277 .flatten()
1278 .map(|e| e.file_name().into_string().unwrap())
1279 .filter(|n| n.contains(".tmp."))
1280 .collect();
1281 assert!(temps.is_empty(), "temp files left behind: {temps:?}");
1282 }
1283
1284 #[test]
1285 fn put_one_defers_the_shard_write_until_flush() {
1286 let _g = crate::test_util::global_state_lock();
1289 let dir = tempdir().unwrap();
1290 let cache_path = dir.path().join("autoloads.rkyv");
1291 let cache = AutoloadCache::open(&cache_path).unwrap();
1292 let sha = source_digest("body");
1293
1294 for i in 0..8 {
1295 cache.put_one(&format!("f{i}"), vec![i], DIR, sha).unwrap();
1296 }
1297 assert!(
1298 !cache_path.exists(),
1299 "put_one wrote the shard before the flush boundary"
1300 );
1301 assert_eq!(cache.get_for_source("f3", DIR, &sha), Some(vec![3]));
1304
1305 cache.flush_pending().unwrap();
1306 assert_eq!(cache.entry_count(), 8);
1307 assert_eq!(cache.get_for_source("f7", DIR, &sha), Some(vec![7]));
1308 }
1309
1310 #[test]
1311 fn flush_preserves_entries_written_by_another_process() {
1312 let _g = crate::test_util::global_state_lock();
1315 let dir = tempdir().unwrap();
1316 let cache_path = dir.path().join("autoloads.rkyv");
1317 let sha = source_digest("body");
1318
1319 let peer = AutoloadCache::open(&cache_path).unwrap();
1320 peer.put_one("peer_fn", vec![9], DIR, sha).unwrap();
1321 peer.flush_pending().unwrap();
1322
1323 let mine = AutoloadCache::open(&cache_path).unwrap();
1324 mine.put_one("my_fn", vec![1], DIR, sha).unwrap();
1325 mine.flush_pending().unwrap();
1326
1327 assert_eq!(mine.get_for_source("peer_fn", DIR, &sha), Some(vec![9]));
1328 assert_eq!(mine.get_for_source("my_fn", DIR, &sha), Some(vec![1]));
1329 }
1330
1331 #[test]
1332 fn remove_drops_a_buffered_entry_before_it_reaches_disk() {
1333 let _g = crate::test_util::global_state_lock();
1337 let dir = tempdir().unwrap();
1338 let cache_path = dir.path().join("autoloads.rkyv");
1339 let cache = AutoloadCache::open(&cache_path).unwrap();
1340 let sha = source_digest("body");
1341
1342 cache.put_one("bad", vec![1], DIR, sha).unwrap();
1343 cache.remove("bad").unwrap();
1344 cache.flush_pending().unwrap();
1345
1346 assert!(cache.get("bad").is_none());
1347 let reopened = AutoloadCache::open(&cache_path).unwrap();
1348 assert!(reopened.get("bad").is_none());
1349 }
1350
1351 #[test]
1352 fn flush_with_the_lock_held_elsewhere_returns_and_keeps_the_batch() {
1353 let _g = crate::test_util::global_state_lock();
1357 let dir = tempdir().unwrap();
1358 let cache_path = dir.path().join("autoloads.rkyv");
1359 let cache = std::sync::Arc::new(AutoloadCache::open(&cache_path).unwrap());
1360 let sha = source_digest("body");
1361 cache.put_one("f", vec![1], DIR, sha).unwrap();
1362
1363 let held = acquire_lock(&cache.lock_path).unwrap();
1366 let (tx, rx) = std::sync::mpsc::channel();
1367 let flusher = std::sync::Arc::clone(&cache);
1368 std::thread::spawn(move || {
1371 let _ = tx.send(flusher.flush_pending());
1372 });
1373 let res = rx
1374 .recv_timeout(std::time::Duration::from_secs(5))
1375 .expect("flush_pending blocked on a lock held elsewhere");
1376 assert!(res.is_ok());
1377 assert!(!cache_path.exists(), "wrote the shard without the lock");
1378 assert_eq!(cache.get_for_source("f", DIR, &sha), Some(vec![1]));
1379
1380 drop(held);
1381 cache.flush_pending().unwrap();
1382 assert!(cache_path.exists());
1383 assert_eq!(cache.entry_count(), 1);
1384 }
1385}