|
|
… |
647 unchanged lines |
| 648 |
648 |
|
} |
| 649 |
649 |
|
} |
| 650 |
650 |
|
|
|
651 |
+ |
// ---------------------------------------------------------------- caching |
|
652 |
+ |
|
|
653 |
+ |
/// A store in front of another (oxygen#68): objects are read through a |
|
654 |
+ |
/// local directory and written through it, to the remote first, so the |
|
655 |
+ |
/// remote always holds everything and the cache can be dropped at any |
|
656 |
+ |
/// time. Objects never change, so a cached one is always right (and every |
|
657 |
+ |
/// read is hashed again above this layer). The cache is bounded: past its |
|
658 |
+ |
/// limit the objects used least recently are removed. An object larger |
|
659 |
+ |
/// than an eighth of the limit is not cached; it is read by range from |
|
660 |
+ |
/// the remote. References are never cached: they are read from and |
|
661 |
+ |
/// written to the remote every time, so compare-and-swap is the remote's. |
|
662 |
+ |
pub struct CachingStore { |
|
663 |
+ |
cache: LocalStore, |
|
664 |
+ |
remote: std::sync::Arc<dyn Store>, |
|
665 |
+ |
limit: u64, |
|
666 |
+ |
/// Bytes in the cache, counted on first need and kept up to date. |
|
667 |
+ |
used: Mutex<Option<u64>>, |
|
668 |
+ |
} |
|
669 |
+ |
|
|
670 |
+ |
/// How stale a cached object's use time may get before a read renews it. |
|
671 |
+ |
const TOUCH_AFTER: Duration = Duration::from_secs(60); |
|
672 |
+ |
|
|
673 |
+ |
impl CachingStore { |
|
674 |
+ |
/// `remote` behind a cache in the directory `dir` of at most `limit` |
|
675 |
+ |
/// bytes. |
|
676 |
+ |
pub fn open( |
|
677 |
+ |
remote: std::sync::Arc<dyn Store>, |
|
678 |
+ |
dir: impl Into<PathBuf>, |
|
679 |
+ |
limit: u64, |
|
680 |
+ |
) -> Result<CachingStore> { |
|
681 |
+ |
Ok(CachingStore { |
|
682 |
+ |
cache: LocalStore::open(dir)?, |
|
683 |
+ |
remote, |
|
684 |
+ |
limit, |
|
685 |
+ |
used: Mutex::new(None), |
|
686 |
+ |
}) |
|
687 |
+ |
} |
|
688 |
+ |
|
|
689 |
+ |
pub fn remote(&self) -> &std::sync::Arc<dyn Store> { |
|
690 |
+ |
&self.remote |
|
691 |
+ |
} |
|
692 |
+ |
|
|
693 |
+ |
/// Bytes the cache holds now. |
|
694 |
+ |
pub fn cached_bytes(&self) -> Result<u64> { |
|
695 |
+ |
let mut used = self.used.lock().unwrap_or_else(|p| p.into_inner()); |
|
696 |
+ |
if used.is_none() { |
|
697 |
+ |
*used = Some(self.cache.list("")?.iter().map(|e| e.size).sum()); |
|
698 |
+ |
} |
|
699 |
+ |
Ok(used.unwrap_or(0)) |
|
700 |
+ |
} |
|
701 |
+ |
|
|
702 |
+ |
/// Whether an object of `size` bytes is kept in the cache at all. |
|
703 |
+ |
fn cacheable(&self, size: u64) -> bool { |
|
704 |
+ |
size <= self.limit / 8 |
|
705 |
+ |
} |
|
706 |
+ |
|
|
707 |
+ |
/// Keep a copy of an object the remote holds. |
|
708 |
+ |
fn keep(&self, key: &str, bytes: &[u8]) -> Result<()> { |
|
709 |
+ |
if !self.cacheable(bytes.len() as u64) { |
|
710 |
+ |
return Ok(()); |
|
711 |
+ |
} |
|
712 |
+ |
match self.cache.put_new(key, bytes) { |
|
713 |
+ |
Ok(()) => self.added(bytes.len() as u64), |
|
714 |
+ |
Err(Error::Exists(_)) => Ok(()), |
|
715 |
+ |
Err(e) => Err(e), |
|
716 |
+ |
} |
|
717 |
+ |
} |
|
718 |
+ |
|
|
719 |
+ |
/// Count `n` new bytes, and remove the least recently used objects |
|
720 |
+ |
/// once the cache is past its limit, down to nine tenths of it. |
|
721 |
+ |
fn added(&self, n: u64) -> Result<()> { |
|
722 |
+ |
let total = { |
|
723 |
+ |
let mut used = self.used.lock().unwrap_or_else(|p| p.into_inner()); |
|
724 |
+ |
let total = match *used { |
|
725 |
+ |
Some(u) => u + n, |
|
726 |
+ |
// Counted now, with the new object already in place. |
|
727 |
+ |
None => self.cache.list("")?.iter().map(|e| e.size).sum(), |
|
728 |
+ |
}; |
|
729 |
+ |
*used = Some(total); |
|
730 |
+ |
total |
|
731 |
+ |
}; |
|
732 |
+ |
if total <= self.limit { |
|
733 |
+ |
return Ok(()); |
|
734 |
+ |
} |
|
735 |
+ |
let mut entries = self.cache.list("")?; |
|
736 |
+ |
entries.sort_by_key(|e| e.modified_ms.unwrap_or(0)); |
|
737 |
+ |
let mut left: u64 = entries.iter().map(|e| e.size).sum(); |
|
738 |
+ |
let target = self.limit / 10 * 9; |
|
739 |
+ |
for e in entries { |
|
740 |
+ |
if left <= target { |
|
741 |
+ |
break; |
|
742 |
+ |
} |
|
743 |
+ |
self.cache.delete(&e.key)?; |
|
744 |
+ |
left -= e.size; |
|
745 |
+ |
} |
|
746 |
+ |
*self.used.lock().unwrap_or_else(|p| p.into_inner()) = Some(left); |
|
747 |
+ |
Ok(()) |
|
748 |
+ |
} |
|
749 |
+ |
|
|
750 |
+ |
/// Mark a cached object used now, at most once a minute. |
|
751 |
+ |
fn touch(&self, key: &str) { |
|
752 |
+ |
let Ok(path) = self.cache.path(key) else { |
|
753 |
+ |
return; |
|
754 |
+ |
}; |
|
755 |
+ |
let stale = fs::metadata(&path) |
|
756 |
+ |
.and_then(|m| m.modified()) |
|
757 |
+ |
.ok() |
|
758 |
+ |
.and_then(|t| t.elapsed().ok()) |
|
759 |
+ |
.is_some_and(|age| age > TOUCH_AFTER); |
|
760 |
+ |
if stale { |
|
761 |
+ |
if let Ok(f) = fs::File::options().append(true).open(&path) { |
|
762 |
+ |
let _ = f.set_modified(std::time::SystemTime::now()); |
|
763 |
+ |
} |
|
764 |
+ |
} |
|
765 |
+ |
} |
|
766 |
+ |
} |
|
767 |
+ |
|
|
768 |
+ |
impl Store for CachingStore { |
|
769 |
+ |
fn describe(&self) -> String { |
|
770 |
+ |
format!( |
|
771 |
+ |
"{} (cached in {})", |
|
772 |
+ |
self.remote.describe(), |
|
773 |
+ |
self.cache.root().display() |
|
774 |
+ |
) |
|
775 |
+ |
} |
|
776 |
+ |
|
|
777 |
+ |
fn has(&self, key: &str) -> Result<bool> { |
|
778 |
+ |
Ok(self.cache.has(key)? || self.remote.has(key)?) |
|
779 |
+ |
} |
|
780 |
+ |
|
|
781 |
+ |
fn get(&self, key: &str) -> Result<Vec<u8>> { |
|
782 |
+ |
match self.cache.get(key) { |
|
783 |
+ |
Ok(bytes) => { |
|
784 |
+ |
self.touch(key); |
|
785 |
+ |
return Ok(bytes); |
|
786 |
+ |
} |
|
787 |
+ |
// Not cached, or removed by another process's eviction meanwhile. |
|
788 |
+ |
Err(Error::NotFound(_)) => {} |
|
789 |
+ |
Err(e) => return Err(e), |
|
790 |
+ |
} |
|
791 |
+ |
let bytes = self.remote.get(key)?; |
|
792 |
+ |
self.keep(key, &bytes)?; |
|
793 |
+ |
Ok(bytes) |
|
794 |
+ |
} |
|
795 |
+ |
|
|
796 |
+ |
fn get_range(&self, key: &str, range: Range<u64>) -> Result<Vec<u8>> { |
|
797 |
+ |
match self.cache.get_range(key, range.clone()) { |
|
798 |
+ |
Ok(bytes) => { |
|
799 |
+ |
self.touch(key); |
|
800 |
+ |
return Ok(bytes); |
|
801 |
+ |
} |
|
802 |
+ |
Err(Error::NotFound(_)) => {} |
|
803 |
+ |
Err(e) if self.cache.has(key)? => return Err(e), |
|
804 |
+ |
Err(_) => {} |
|
805 |
+ |
} |
|
806 |
+ |
// A small object is fetched whole and kept; a large one is read by |
|
807 |
+ |
// range, every time. |
|
808 |
+ |
match self.remote.stat(key)? { |
|
809 |
+ |
Some(e) if self.cacheable(e.size) => { |
|
810 |
+ |
let bytes = self.get(key)?; |
|
811 |
+ |
let len = bytes.len() as u64; |
|
812 |
+ |
if range.end > len || range.start > range.end { |
|
813 |
+ |
return Err(Error::Backend(format!( |
|
814 |
+ |
"{key}: bytes {}..{} asked of an object of {len} bytes", |
|
815 |
+ |
range.start, range.end |
|
816 |
+ |
))); |
|
817 |
+ |
} |
|
818 |
+ |
Ok(bytes[range.start as usize..range.end as usize].to_vec()) |
|
819 |
+ |
} |
|
820 |
+ |
Some(_) => self.remote.get_range(key, range), |
|
821 |
+ |
None => Err(Error::NotFound(key.to_string())), |
|
822 |
+ |
} |
|
823 |
+ |
} |
|
824 |
+ |
|
|
825 |
+ |
fn put_new(&self, key: &str, bytes: &[u8]) -> Result<()> { |
|
826 |
+ |
self.remote.put_new(key, bytes)?; |
|
827 |
+ |
self.keep(key, bytes) |
|
828 |
+ |
} |
|
829 |
+ |
|
|
830 |
+ |
fn put_many_new(&self, objects: &[(String, Vec<u8>)]) -> Result<()> { |
|
831 |
+ |
self.remote.put_many_new(objects)?; |
|
832 |
+ |
for (key, bytes) in objects { |
|
833 |
+ |
self.keep(key, bytes)?; |
|
834 |
+ |
} |
|
835 |
+ |
Ok(()) |
|
836 |
+ |
} |
|
837 |
+ |
|
|
838 |
+ |
fn put_new_file(&self, key: &str, path: &Path) -> Result<()> { |
|
839 |
+ |
let size = fs::metadata(path)?.len(); |
|
840 |
+ |
let copy = if self.cacheable(size) { |
|
841 |
+ |
Some(fs::read(path)?) |
|
842 |
+ |
} else { |
|
843 |
+ |
None |
|
844 |
+ |
}; |
|
845 |
+ |
self.remote.put_new_file(key, path)?; |
|
846 |
+ |
match copy { |
|
847 |
+ |
Some(bytes) => self.keep(key, &bytes), |
|
848 |
+ |
None => Ok(()), |
|
849 |
+ |
} |
|
850 |
+ |
} |
|
851 |
+ |
|
|
852 |
+ |
fn list(&self, prefix: &str) -> Result<Vec<Entry>> { |
|
853 |
+ |
self.remote.list(prefix) |
|
854 |
+ |
} |
|
855 |
+ |
|
|
856 |
+ |
fn stat(&self, key: &str) -> Result<Option<Entry>> { |
|
857 |
+ |
self.remote.stat(key) |
|
858 |
+ |
} |
|
859 |
+ |
|
|
860 |
+ |
fn delete(&self, key: &str) -> Result<()> { |
|
861 |
+ |
self.remote.delete(key)?; |
|
862 |
+ |
let size = self.cache.stat(key)?.map(|e| e.size); |
|
863 |
+ |
self.cache.delete(key)?; |
|
864 |
+ |
if let Some(n) = size { |
|
865 |
+ |
let mut used = self.used.lock().unwrap_or_else(|p| p.into_inner()); |
|
866 |
+ |
if let Some(u) = used.as_mut() { |
|
867 |
+ |
*u = u.saturating_sub(n); |
|
868 |
+ |
} |
|
869 |
+ |
} |
|
870 |
+ |
Ok(()) |
|
871 |
+ |
} |
|
872 |
+ |
|
|
873 |
+ |
fn read_ref(&self, key: &str) -> Result<Option<(Vec<u8>, Version)>> { |
|
874 |
+ |
self.remote.read_ref(key) |
|
875 |
+ |
} |
|
876 |
+ |
|
|
877 |
+ |
fn write_ref(&self, key: &str, bytes: &[u8], expected: Option<&Version>) -> Result<Version> { |
|
878 |
+ |
self.remote.write_ref(key, bytes, expected) |
|
879 |
+ |
} |
|
880 |
+ |
} |
|
881 |
+ |
|
| 651 |
882 |
|
// ---------------------------------------------------------------- conformance |
| 652 |
883 |
|
|
| 653 |
884 |
|
/// Exercise a store under `prefix` (which must be free, and is left |
|
|
… |
155 unchanged lines |
| 809 |
1040 |
|
assert!(check_key(bad).is_err(), "{bad:?}"); |
| 810 |
1041 |
|
} |
| 811 |
1042 |
|
assert!(check_key(&"x".repeat(MAX_KEY_LEN + 1)).is_err()); |
|
1043 |
+ |
} |
|
1044 |
+ |
|
|
1045 |
+ |
#[test] |
|
1046 |
+ |
fn a_caching_store_conforms_and_reads_and_writes_through() { |
|
1047 |
+ |
let dir = tempfile::tempdir().unwrap(); |
|
1048 |
+ |
let remote: Arc<dyn Store> = Arc::new(MemoryStore::new()); |
|
1049 |
+ |
let store = CachingStore::open(remote.clone(), dir.path().join("cache"), 8 << 20).unwrap(); |
|
1050 |
+ |
assert_eq!(conformance(&store, "probe").unwrap().len(), 8); |
|
1051 |
+ |
assert_eq!( |
|
1052 |
+ |
store.cached_bytes().unwrap(), |
|
1053 |
+ |
0, |
|
1054 |
+ |
"the probe left nothing cached" |
|
1055 |
+ |
); |
|
1056 |
+ |
|
|
1057 |
+ |
// Written through: the remote has it, and so does the cache. |
|
1058 |
+ |
store.put_new("loose/aa", b"written").unwrap(); |
|
1059 |
+ |
assert_eq!(remote.get("loose/aa").unwrap(), b"written"); |
|
1060 |
+ |
assert!(store.cache.has("loose/aa").unwrap()); |
|
1061 |
+ |
// Read through: an object only the remote has is kept on first read. |
|
1062 |
+ |
remote.put_new("loose/bb", b"remote only").unwrap(); |
|
1063 |
+ |
assert!(!store.cache.has("loose/bb").unwrap()); |
|
1064 |
+ |
assert_eq!(store.get_range("loose/bb", 0..6).unwrap(), b"remote"); |
|
1065 |
+ |
assert!(store.cache.has("loose/bb").unwrap()); |
|
1066 |
+ |
// A cached object read after the remote lost it is still served. |
|
1067 |
+ |
remote.delete("loose/bb").unwrap(); |
|
1068 |
+ |
assert_eq!(store.get("loose/bb").unwrap(), b"remote only"); |
|
1069 |
+ |
// References are always the remote's. |
|
1070 |
+ |
let v = remote.write_ref("refs", b"one", None).unwrap(); |
|
1071 |
+ |
assert_eq!( |
|
1072 |
+ |
store.read_ref("refs").unwrap(), |
|
1073 |
+ |
Some((b"one".to_vec(), v.clone())) |
|
1074 |
+ |
); |
|
1075 |
+ |
remote.write_ref("refs", b"two", Some(&v)).unwrap(); |
|
1076 |
+ |
assert_eq!(store.read_ref("refs").unwrap().unwrap().0, b"two"); |
|
1077 |
+ |
assert!(matches!( |
|
1078 |
+ |
store.write_ref("refs", b"three", Some(&v)), |
|
1079 |
+ |
Err(Error::Conflict(_)) |
|
1080 |
+ |
)); |
|
1081 |
+ |
assert!(!store.cache.has("refs").unwrap()); |
|
1082 |
+ |
} |
|
1083 |
+ |
|
|
1084 |
+ |
#[test] |
|
1085 |
+ |
fn a_caching_store_stays_within_its_limit() { |
|
1086 |
+ |
let dir = tempfile::tempdir().unwrap(); |
|
1087 |
+ |
let remote: Arc<dyn Store> = Arc::new(MemoryStore::new()); |
|
1088 |
+ |
let store = CachingStore::open(remote.clone(), dir.path().join("cache"), 8000).unwrap(); |
|
1089 |
+ |
for i in 0..20 { |
|
1090 |
+ |
store |
|
1091 |
+ |
.put_new(&format!("loose/{i:02}"), &[i as u8; 900]) |
|
1092 |
+ |
.unwrap(); |
|
1093 |
+ |
assert!(store.cached_bytes().unwrap() <= 8000); |
|
1094 |
+ |
} |
|
1095 |
+ |
assert_eq!( |
|
1096 |
+ |
remote.list("loose/").unwrap().len(), |
|
1097 |
+ |
20, |
|
1098 |
+ |
"the remote holds everything" |
|
1099 |
+ |
); |
|
1100 |
+ |
let cached = store.cache.list("loose/").unwrap(); |
|
1101 |
+ |
assert!(cached.len() < 20 && cached.len() >= 7, "{}", cached.len()); |
|
1102 |
+ |
assert!( |
|
1103 |
+ |
cached.iter().any(|e| e.key == "loose/19"), |
|
1104 |
+ |
"the newest is kept" |
|
1105 |
+ |
); |
|
1106 |
+ |
for i in 0..20 { |
|
1107 |
+ |
assert_eq!( |
|
1108 |
+ |
store.get(&format!("loose/{i:02}")).unwrap(), |
|
1109 |
+ |
vec![i as u8; 900] |
|
1110 |
+ |
); |
|
1111 |
+ |
} |
|
1112 |
+ |
// An object over an eighth of the limit is read by range, not kept. |
|
1113 |
+ |
remote.put_new("pack-big", &[7u8; 2000]).unwrap(); |
|
1114 |
+ |
assert_eq!(store.get_range("pack-big", 10..20).unwrap(), vec![7u8; 10]); |
|
1115 |
+ |
assert_eq!(store.get("pack-big").unwrap().len(), 2000); |
|
1116 |
+ |
assert!(!store.cache.has("pack-big").unwrap()); |
|
1117 |
+ |
// Counting from scratch agrees with what was kept track of. |
|
1118 |
+ |
let fresh = CachingStore::open(remote, dir.path().join("cache"), 8000).unwrap(); |
|
1119 |
+ |
assert_eq!(fresh.cached_bytes().unwrap(), store.cached_bytes().unwrap()); |
| 812 |
1120 |
|
} |
| 813 |
1121 |
|
|
| 814 |
1122 |
|
#[test] |
|
|
… |
104 unchanged lines |