1use std::collections::HashMap;
2use std::fs::{DirEntry, File};
3use std::io::{self, Cursor, ErrorKind, Read, Seek, SeekFrom, Write};
4use std::mem::size_of;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use async_trait::async_trait;
9use base64::Engine;
10use base64::engine::GeneralPurpose;
11use base64::engine::general_purpose::URL_SAFE;
12use tokio::sync::RwLock;
13use tracing::{debug, error};
14use xet_core_structures::merklehash::MerkleHash;
15use xet_runtime::config::XetConfig;
16use xet_runtime::error_printer::ErrorPrinter;
17use xet_runtime::file_utils::SafeFileCreator;
18use xet_runtime::utils::output_bytes;
19
20use self::cache_file_header::CacheFileHeader;
21use self::cache_item::{CacheItem, VerificationCell};
22use super::error::ChunkCacheError;
23use super::{CacheConfig, CacheRange, ChunkCache};
24use crate::cas_types::{ChunkRange, Key};
25
26mod cache_file_header;
27mod cache_item;
28pub mod test_utils;
29
30pub(crate) const BASE64_ENGINE: GeneralPurpose = URL_SAFE;
32const PREFIX_DIR_NAME_LEN: usize = 2;
33
34type OptionResult<T, E> = Result<Option<T>, E>;
35
36#[derive(Debug, Clone)]
37struct CacheState {
38 inner: HashMap<Key, Vec<VerificationCell<CacheItem>>>,
39 num_items: usize,
40 total_bytes: u64,
41}
42
43impl CacheState {
44 fn new(state: HashMap<Key, Vec<VerificationCell<CacheItem>>>, num_items: usize, total_bytes: u64) -> Self {
45 Self {
46 inner: state,
47 num_items,
48 total_bytes,
49 }
50 }
51
52 fn find_match(&self, key: &Key, range: &ChunkRange) -> Option<VerificationCell<CacheItem>> {
53 let items = self.inner.get(key)?;
54
55 for item in items.iter() {
57 if item.range.start <= range.start && range.end <= item.range.end {
58 return Some(item.clone());
59 }
60 }
61 None
62 }
63
64 fn evict_to_capacity(
70 &mut self,
71 max_total_bytes: u64,
72 ) -> Result<Vec<(Key, VerificationCell<CacheItem>)>, ChunkCacheError> {
73 let original_total_bytes = self.total_bytes;
74 let mut ret = Vec::new();
75
76 while self.total_bytes > max_total_bytes {
77 let Some((key, idx)) = self.random_item() else {
78 error!("attempted to evict item, but no item could be found to be evicted");
79 break;
80 };
81 let items = self.inner.get_mut(&key).ok_or(ChunkCacheError::Infallible)?;
82 let cache_item = items.swap_remove(idx);
83 let len = cache_item.len;
84
85 if items.is_empty() {
86 self.inner.remove(&key);
87 }
88
89 ret.push((key, cache_item));
90
91 self.total_bytes -= len;
92 self.num_items -= 1;
93 }
94 debug!(
95 "cache evicting {} items totaling {}",
96 ret.len(),
97 output_bytes(original_total_bytes - self.total_bytes)
98 );
99
100 Ok(ret)
101 }
102
103 fn random_item(&self) -> Option<(Key, usize)> {
105 debug_assert_eq!(
106 self.inner.values().map(|v| v.len()).sum::<usize>(),
107 self.num_items,
108 "real num items != stored num items"
109 );
110
111 if self.num_items == 0 {
112 error!("cache random_item for eviction: no items in cache");
113 return None;
114 }
115 let random_item = rand::random::<u32>() as usize % self.num_items;
116 let mut count = 0;
117 for (key, items) in self.inner.iter() {
118 if random_item < count + items.len() {
119 return Some((key.clone(), random_item - count));
120 }
121 count += items.len();
122 }
123 error!("cache random_item for eviction: tried to return random item error not enough items");
125 None
126 }
127}
128
129#[derive(Debug, Clone)]
131pub struct DiskCache {
132 cache_root: PathBuf,
133 capacity: u64,
134 state: Arc<RwLock<CacheState>>,
135}
136
137#[cfg(feature = "analysis")]
139impl DiskCache {
140 pub async fn print(&self) {
141 let state = self.state.read().await;
142 let total_num_items = state.num_items;
143 let total_total_bytes = state.total_bytes;
144
145 println!(
146 "total items: {}, total bytes {} for the whole cache",
147 total_num_items,
148 output_bytes(total_total_bytes)
149 );
150
151 for (key, items) in state.inner.iter() {
152 println!();
153 let num_items = items.len();
154 let total_bytes: usize = items.iter().map(|item| item.len).fold(0usize, |acc, len| acc + len as usize);
155 println!("key: {key}");
156 println!("\ttotal items: {}, total bytes {} for key {key}", num_items, output_bytes(total_bytes as u64));
157 println!();
158 for item in items.iter() {
159 println!(
160 "\titem: chunk range [{}-{}) ; len({}); checksum({})",
161 item.range.start,
162 item.range.end,
163 output_bytes(item.len),
164 item.checksum,
165 );
166 }
167 }
168 }
169}
170
171impl DiskCache {
172 pub async fn num_items(&self) -> usize {
173 self.state.read().await.num_items
174 }
175
176 pub async fn total_bytes(&self) -> u64 {
177 self.state.read().await.total_bytes
178 }
179
180 pub fn initialize(xet_config: &XetConfig, config: &CacheConfig) -> Result<Self, ChunkCacheError> {
209 if config.cache_size == 0 {
210 return Err(ChunkCacheError::InvalidArguments);
211 }
212 let capacity = config.cache_size;
213 let cache_root = config.cache_directory.clone();
214
215 let state = Self::initialize_state(&cache_root, capacity, xet_config)?;
217
218 Ok(Self {
219 state: Arc::new(RwLock::new(state)),
220 cache_root: config.cache_directory.clone(),
221 capacity,
222 })
223 }
224
225 fn initialize_state(
226 cache_root: &PathBuf,
227 capacity: u64,
228 xet_config: &XetConfig,
229 ) -> Result<CacheState, ChunkCacheError> {
230 let mut state = HashMap::new();
231 let mut total_bytes = 0;
232 let mut num_items = 0;
233 let max_num_bytes = 2 * capacity;
234
235 let Some(cache_root_readdir) = read_dir(cache_root)? else {
236 return Ok(CacheState::new(state, 0, 0));
237 };
238
239 for key_prefix_dir in cache_root_readdir {
242 let Some(key_prefix_dir) = is_ok_dir(key_prefix_dir)? else {
243 continue;
244 };
245
246 let key_prefix_dir_name = key_prefix_dir.file_name();
247 if key_prefix_dir_name.as_encoded_bytes().len() != PREFIX_DIR_NAME_LEN {
248 debug!("prefix dir name len != {PREFIX_DIR_NAME_LEN}");
249 continue;
250 }
251
252 let Some(key_prefix_readdir) = read_dir(key_prefix_dir.path())? else {
253 continue;
254 };
255
256 for key_dir in key_prefix_readdir {
258 let key_dir = match is_ok_dir(key_dir) {
259 Ok(Some(dirent)) => dirent,
260 Ok(None) => continue,
261 Err(e) => return Err(e),
262 };
263
264 let key_dir_name = key_dir.file_name();
265
266 debug_assert_eq!(
268 key_dir_name.as_encoded_bytes()[..PREFIX_DIR_NAME_LEN].to_ascii_uppercase(),
269 key_prefix_dir_name.as_encoded_bytes().to_ascii_uppercase(),
270 "{key_dir_name:?}",
271 );
272
273 let key = match try_parse_key(key_dir_name.as_encoded_bytes()) {
274 Ok(key) => key,
275 Err(e) => {
276 debug!("failed to decoded a directory name as a key: {e}");
277 continue;
278 },
279 };
280
281 let mut items = Vec::new();
282
283 let key_readdir = match read_dir(key_dir.path()) {
284 Ok(Some(krd)) => krd,
285 Ok(None) => continue,
286 Err(e) => return Err(e),
287 };
288
289 for item in key_readdir {
291 let cache_item = match try_parse_cache_file(item, capacity, xet_config) {
292 Ok(Some(ci)) => ci,
293 Ok(None) => continue,
294 Err(e) => return Err(e),
295 };
296
297 total_bytes += cache_item.len;
298 num_items += 1;
299 items.push(VerificationCell::new_unverified(cache_item));
300
301 if total_bytes >= max_num_bytes {
303 state.insert(key, items);
304 return Ok(CacheState::new(state, num_items, total_bytes));
305 }
306 }
307
308 if !items.is_empty() {
309 state.insert(key, items);
310 }
311 }
312 }
313
314 Ok(CacheState::new(state, num_items, total_bytes))
315 }
316
317 async fn get_impl(&self, key: &Key, range: &ChunkRange) -> OptionResult<CacheRange, ChunkCacheError> {
318 if range.start >= range.end {
319 return Err(ChunkCacheError::InvalidArguments);
320 }
321
322 loop {
323 let Some(cache_item) = self.state.read().await.find_match(key, range) else {
324 return Ok(None);
325 };
326
327 let path = self.item_path(key, &cache_item)?;
328
329 let mut file = match File::open(&path) {
330 Ok(file) => file,
331 Err(e) => match e.kind() {
332 ErrorKind::NotFound => {
333 self.remove_item(key, &cache_item).await?;
334 continue;
335 },
336 _ => return Err(e.into()),
337 },
338 };
339
340 if !cache_item.is_verified() {
341 let checksum = crc32_from_reader(&mut file)?;
342 if checksum == cache_item.checksum {
343 cache_item.verify();
344 file.rewind()?;
345 } else {
346 debug!("computed checksum {checksum} mismatch on cache item {key}/{cache_item}");
347 self.remove_item(key, &cache_item).await?;
348 continue;
349 }
350 }
351
352 let mut file_reader = std::io::BufReader::new(file);
353
354 let Ok(header) = CacheFileHeader::deserialize(&mut file_reader)
355 .debug_error(format!("failed to deserialize cache file header on path: {path:?}"))
356 else {
357 self.remove_item(key, &cache_item).await?;
358 continue;
359 };
360
361 let start = cache_item.range.start;
362 let result_buf = get_range_from_cache_file(&header, &mut file_reader, range, start)?;
363 return Ok(Some(result_buf));
364 }
365 }
366
367 async fn put_impl(
368 &self,
369 key: &Key,
370 range: &ChunkRange,
371 chunk_byte_indices: &[u32],
372 data: &[u8],
373 ) -> Result<(), ChunkCacheError> {
374 if range.start >= range.end
375 || chunk_byte_indices.len() != (range.end - range.start + 1) as usize
376 || chunk_byte_indices[0] != 0
378 || *chunk_byte_indices.last().unwrap() as usize != data.len()
379 || !strictly_increasing(chunk_byte_indices)
380 {
381 return Err(ChunkCacheError::InvalidArguments);
382 }
383
384 while let Some(cache_item) = self.state.read().await.find_match(key, range) {
386 if self.validate_match(key, range, chunk_byte_indices, data, &cache_item).await? {
387 return Ok(());
388 }
389 }
390
391 let header = CacheFileHeader::new(chunk_byte_indices);
392 let mut header_buf = Vec::with_capacity(header.header_len());
393 header.serialize(&mut header_buf)?;
394 let len = (header_buf.len() + data.len()) as u64;
395 if len > self.capacity {
396 return Ok(());
398 }
399
400 let checksum = {
401 let mut hasher = crc32fast::Hasher::new();
402 hasher.update(&header_buf);
403 hasher.update(data);
404 hasher.finalize()
405 };
406
407 let cache_item = CacheItem {
408 range: *range,
409 len,
410 checksum,
411 };
412
413 let path = self.item_path(key, &cache_item)?;
415 let mut fw = SafeFileCreator::new(path)?;
416 fw.write_all(&header_buf)?;
417 fw.write_all(data)?;
418
419 let mut state_write = self.state.write().await;
422
423 if state_write.find_match(key, range).is_some() {
428 fw.abort()?;
431 return Ok(());
432 }
433 fw.close()?;
434
435 let evicted_paths = state_write.evict_to_capacity(self.capacity - cache_item.len)?;
437
438 state_write.num_items += 1;
440 state_write.total_bytes += cache_item.len;
441 let item_set = state_write.inner.entry(key.clone()).or_default();
442 item_set.push(VerificationCell::new_verified(cache_item));
443
444 drop(state_write);
446
447 for (key, cache_item) in evicted_paths {
449 let path = self.item_path(&key, &cache_item)?;
450 remove_file(&path)?;
451 let dir_path = path.parent().ok_or(ChunkCacheError::Infallible)?;
453 check_remove_dir(dir_path)?;
454 }
455
456 Ok(())
457 }
458
459 async fn validate_match(
462 &self,
463 key: &Key,
464 range: &ChunkRange,
465 chunk_byte_indices: &[u32],
466 data: &[u8],
467 cache_item: &VerificationCell<CacheItem>,
468 ) -> Result<bool, ChunkCacheError> {
469 if range.start < cache_item.range.start || range.end > cache_item.range.end {
471 return Err(ChunkCacheError::BadRange);
472 }
473
474 let path = self.item_path(key, cache_item)?;
476
477 let Ok(mut file) = File::open(path) else {
478 self.remove_item(key, cache_item).await?;
479 return Ok(false);
480 };
481 let md = file.metadata()?;
482 if md.len() != cache_item.len {
483 self.remove_item(key, cache_item).await?;
484 return Ok(false);
485 }
486 let mut buf = Vec::with_capacity(md.len() as usize);
487 file.read_to_end(&mut buf)?;
488 let checksum = crc32fast::hash(&buf);
489 if checksum != cache_item.checksum {
490 self.remove_item(key, cache_item).await?;
491 return Ok(false);
492 }
493 let mut reader = Cursor::new(buf);
494 let Ok(header) = CacheFileHeader::deserialize(&mut reader) else {
495 self.remove_item(key, cache_item).await?;
496 return Ok(false);
497 };
498
499 let idx_start = (range.start - cache_item.range.start) as usize;
504 let idx_end = (range.end - cache_item.range.start + 1) as usize;
505 for i in idx_start..idx_end - 1 {
506 let stored_diff = header.chunk_byte_indices[i + 1] - header.chunk_byte_indices[i];
507 let given_diff = chunk_byte_indices[i + 1 - idx_start] - chunk_byte_indices[i - idx_start];
508 if stored_diff != given_diff {
509 debug!(
510 "failed to match chunk lens for these chunk offsets {} {:?}\n{} {:?}",
511 cache_item.range,
512 &header.chunk_byte_indices[idx_start..idx_end],
513 range,
514 chunk_byte_indices
515 );
516 return Err(ChunkCacheError::InvalidArguments);
517 }
518 }
519
520 let stored = get_range_from_cache_file(&header, &mut reader, range, cache_item.range.start)?;
521 if data != stored.data {
522 return Err(ChunkCacheError::InvalidArguments);
523 }
524 Ok(true)
525 }
526
527 async fn remove_item(&self, key: &Key, cache_item: &VerificationCell<CacheItem>) -> Result<(), ChunkCacheError> {
529 {
530 let mut state = self.state.write().await;
531 if let Some(items) = state.inner.get_mut(key) {
532 let idx = match index_of(items, cache_item) {
533 Some(idx) => idx,
534 None => return Ok(()),
536 };
537
538 items.swap_remove(idx);
539 if items.is_empty() {
540 state.inner.remove(key);
541 }
542 state.total_bytes -= cache_item.len;
543 state.num_items -= 1;
544 }
545 }
546
547 let path = self.item_path(key, cache_item)?;
548
549 if !path.exists() {
550 return Ok(());
551 }
552 remove_file(&path)?;
553 let dir_path = path.parent().ok_or(ChunkCacheError::Infallible)?;
554 check_remove_dir(dir_path)
555 }
556
557 fn item_path(&self, key: &Key, cache_item: &CacheItem) -> Result<PathBuf, ChunkCacheError> {
558 Ok(self.cache_root.join(key_dir(key)).join(cache_item.file_name()?))
559 }
560}
561
562fn crc32_from_reader(reader: &mut impl Read) -> Result<u32, ChunkCacheError> {
563 const CRC_BUFFER_SIZE: usize = 4096;
564 let mut buf = [0u8; CRC_BUFFER_SIZE];
565 let mut hasher = crc32fast::Hasher::new();
566 loop {
567 let num_read = reader.read(&mut buf)?;
568 if num_read == 0 {
569 break;
570 }
571 hasher.update(&buf[..num_read])
572 }
573 Ok(hasher.finalize())
574}
575
576#[inline]
577fn index_of<T: PartialEq>(list: &[T], value: &T) -> Option<usize> {
578 for (i, list_value) in list.iter().enumerate() {
579 if list_value == value {
580 return Some(i);
581 }
582 }
583 None
584}
585
586fn strictly_increasing(chunk_byte_indices: &[u32]) -> bool {
587 for i in 1..chunk_byte_indices.len() {
588 if chunk_byte_indices[i - 1] >= chunk_byte_indices[i] {
589 return false;
590 }
591 }
592 true
593}
594
595fn get_range_from_cache_file<R: Read + Seek>(
596 header: &CacheFileHeader,
597 file_contents: &mut R,
598 range: &ChunkRange,
599 start: u32,
600) -> Result<CacheRange, ChunkCacheError> {
601 let start_idx = (range.start - start) as usize;
602 let end_idx = (range.end - start) as usize;
603 let start_byte = header.chunk_byte_indices.get(start_idx).ok_or(ChunkCacheError::BadRange)?;
604 let end_byte = header.chunk_byte_indices.get(end_idx).ok_or(ChunkCacheError::BadRange)?;
605 file_contents.seek(SeekFrom::Start((*start_byte as usize + header.header_len()) as u64))?;
606 let mut data = vec![0; (end_byte - start_byte) as usize];
607 file_contents.read_exact(&mut data)?;
608 let offsets: Vec<u32> = header.chunk_byte_indices[start_idx..=end_idx]
609 .iter()
610 .map(|v| *v - header.chunk_byte_indices[start_idx])
611 .collect();
612
613 debug_assert_eq!(range.end - range.start, offsets.len() as u32 - 1);
614
615 Ok(CacheRange {
616 offsets,
617 data,
618 range: *range,
619 })
620}
621
622fn read_dir(path: impl AsRef<Path>) -> OptionResult<std::fs::ReadDir, ChunkCacheError> {
625 match std::fs::read_dir(path) {
626 Ok(rd) => Ok(Some(rd)),
627 Err(e) => {
628 if e.kind() == ErrorKind::NotFound {
629 Ok(None)
630 } else {
631 Err(e.into())
632 }
633 },
634 }
635}
636
637fn is_ok_dir(dir_result: Result<DirEntry, io::Error>) -> OptionResult<DirEntry, ChunkCacheError> {
643 let dirent = match dir_result {
644 Ok(kd) => kd,
645 Err(e) => {
646 if e.kind() == ErrorKind::NotFound {
647 return Ok(None);
648 }
649 return Err(e.into());
650 },
651 };
652 let md = match dirent.metadata() {
653 Ok(md) => md,
654 Err(e) => {
655 if e.kind() == ErrorKind::NotFound {
656 return Ok(None);
657 }
658 return Err(e.into());
659 },
660 };
661 if !md.is_dir() {
662 debug!("CACHE: expected directory at {:?}, is not directory", dirent.path());
663 return Ok(None);
664 }
665 Ok(Some(dirent))
666}
667
668fn try_parse_cache_file(
672 file_result: io::Result<DirEntry>,
673 capacity: u64,
674 config: &XetConfig,
675) -> OptionResult<CacheItem, ChunkCacheError> {
676 let item = match file_result {
677 Ok(item) => item,
678 Err(e) => {
679 if e.kind() == ErrorKind::NotFound {
680 return Ok(None);
681 }
682 return Err(e.into());
683 },
684 };
685 let md = match item.metadata() {
686 Ok(md) => md,
687 Err(e) => {
688 if e.kind() == ErrorKind::NotFound {
689 return Ok(None);
690 }
691 return Err(e.into());
692 },
693 };
694
695 if !md.is_file() {
696 return Ok(None);
697 }
698 if md.len() > config.chunk_cache.size_bytes {
699 return Err(ChunkCacheError::general(format!(
700 "Cache directory contains a file larger than {} GB, cache directory state is invalid",
701 (config.chunk_cache.size_bytes as f64 / (1 << 30) as f64)
702 )));
703 }
704
705 if md.len() > capacity {
707 return Ok(None);
708 }
709
710 let cache_item = match CacheItem::parse(item.file_name().as_encoded_bytes())
711 .debug_error("failed to decode a file name as a cache item")
712 {
713 Ok(i) => i,
714 Err(e) => {
715 debug!("not a valid cache file, removing: {:?} {e:?}", item.file_name());
716 remove_file(item.path())?;
717 return Ok(None);
718 },
719 };
720 if md.len() != cache_item.len {
721 debug!(
723 "cache file len {} does not match expected length {}, removing path: {:?}",
724 md.len(),
725 cache_item.len,
726 item.path()
727 );
728 remove_file(item.path())?;
729 return Ok(None);
730 }
731 Ok(Some(cache_item))
732}
733
734fn remove_file(path: impl AsRef<Path>) -> Result<(), ChunkCacheError> {
736 if let Err(e) = std::fs::remove_file(path)
737 && e.kind() != ErrorKind::NotFound
738 {
739 return Err(e.into());
740 }
741 Ok(())
742}
743
744fn remove_dir(path: impl AsRef<Path>) -> Result<(), ChunkCacheError> {
746 if let Err(e) = std::fs::remove_dir(path)
747 && e.kind() != ErrorKind::NotFound
748 {
749 return Err(e.into());
750 }
751 Ok(())
752}
753
754fn check_remove_dir(dir_path: impl AsRef<Path>) -> Result<(), ChunkCacheError> {
758 let readdir = match read_dir(&dir_path)? {
759 Some(rd) => rd,
760 None => return Ok(()),
761 };
762 if readdir.peekable().peek().is_some() {
763 return Ok(());
764 }
765 remove_dir(&dir_path)?;
767
768 let prefix_dir = dir_path.as_ref().parent().ok_or(ChunkCacheError::Infallible)?;
770
771 let prefix_readdir = match read_dir(prefix_dir)? {
772 Some(prd) => prd,
773 None => return Ok(()),
774 };
775 if prefix_readdir.peekable().peek().is_some() {
776 return Ok(());
777 }
778 remove_dir(prefix_dir)
780}
781
782fn try_parse_key(file_name: &[u8]) -> Result<Key, ChunkCacheError> {
785 let buf = BASE64_ENGINE.decode(file_name)?;
786 let hash = MerkleHash::from_slice(&buf[..size_of::<MerkleHash>()])?;
787 let prefix = String::from(std::str::from_utf8(&buf[size_of::<MerkleHash>()..])?);
788 Ok(Key { prefix, hash })
789}
790
791fn key_dir(key: &Key) -> PathBuf {
794 let prefix_bytes = key.prefix.as_bytes();
795 let mut buf = vec![0u8; size_of::<MerkleHash>() + prefix_bytes.len()];
796 buf[..size_of::<MerkleHash>()].copy_from_slice(key.hash.as_bytes());
797 buf[size_of::<MerkleHash>()..].copy_from_slice(prefix_bytes);
798 let encoded = BASE64_ENGINE.encode(&buf);
799 let prefix_dir = &encoded[..PREFIX_DIR_NAME_LEN];
800 let dir_str = format!("{prefix_dir}/{encoded}");
801 PathBuf::from(dir_str)
802}
803
804#[async_trait]
805impl ChunkCache for DiskCache {
806 async fn get(&self, key: &Key, range: &ChunkRange) -> Result<Option<CacheRange>, ChunkCacheError> {
807 self.get_impl(key, range).await
808 }
809
810 async fn put(
811 &self,
812 key: &Key,
813 range: &ChunkRange,
814 chunk_byte_indices: &[u32],
815 data: &[u8],
816 ) -> Result<(), ChunkCacheError> {
817 self.put_impl(key, range, chunk_byte_indices, data).await
818 }
819}
820
821#[cfg(test)]
822mod tests {
823 use std::collections::BTreeSet;
824
825 use rand::SeedableRng;
826 use rand::rngs::StdRng;
827 use tempfile::TempDir;
828 use xet_runtime::config::XetConfig;
829 use xet_runtime::utils::output_bytes;
830
831 use super::super::{CacheConfig, ChunkCache};
832 use super::test_utils::*;
833 use super::{DiskCache, try_parse_key};
834 use crate::cas_types::{ChunkRange, Key};
835
836 const RANDOM_SEED: u64 = 9089 << 20 | 120043;
837
838 const DEFAULT_CHUNK_CACHE_CAPACITY: u64 = 10_000_000_000;
839
840 #[tokio::test]
841 async fn test_get_cache_empty() {
842 let mut rng = StdRng::seed_from_u64(RANDOM_SEED);
843 let cache_root = TempDir::new().unwrap();
844 let config = CacheConfig {
845 cache_directory: cache_root.path().to_path_buf(),
846 cache_size: DEFAULT_CHUNK_CACHE_CAPACITY,
847 };
848 let xet_config = XetConfig::new();
849 let cache = DiskCache::initialize(&xet_config, &config).unwrap();
850 assert!(
851 cache
852 .get(&random_key(&mut rng), &random_range(&mut rng))
853 .await
854 .unwrap()
855 .is_none()
856 );
857 }
858
859 #[tokio::test]
860 async fn test_put_get_simple() {
861 let mut rng = StdRng::seed_from_u64(RANDOM_SEED);
862 let cache_root = TempDir::new().unwrap();
863 let config = CacheConfig {
864 cache_directory: cache_root.path().to_path_buf(),
865 cache_size: DEFAULT_CHUNK_CACHE_CAPACITY,
866 };
867 let xet_config = XetConfig::new();
868 let cache = DiskCache::initialize(&xet_config, &config).unwrap();
869
870 let key = random_key(&mut rng);
871 let range = ChunkRange::new(0, 4);
872 let (chunk_byte_indices, data) = random_bytes(&mut rng, &range, RANGE_LEN);
873 let put_result = cache.put(&key, &range, &chunk_byte_indices, data.as_slice()).await;
874 assert!(put_result.is_ok(), "{put_result:?}");
875
876 print_directory_contents(cache_root.as_ref());
877
878 let cache_result = cache.get(&key, &range).await.unwrap();
880 assert!(cache_result.is_some());
881 let cache_range = cache_result.unwrap();
882 assert_eq!(cache_range.data, data);
883 assert_eq!(cache_range.range, range);
884 assert_eq!(cache_range.offsets, chunk_byte_indices);
885
886 let miss_range = ChunkRange::new(100, 101);
887 assert!(cache.get(&key, &miss_range).await.unwrap().is_none());
889 }
890
891 #[tokio::test]
892 async fn test_put_get_subrange() {
893 let mut rng = StdRng::seed_from_u64(RANDOM_SEED);
894 let cache_root = TempDir::new().unwrap();
895 let config = CacheConfig {
896 cache_directory: cache_root.path().to_path_buf(),
897 cache_size: DEFAULT_CHUNK_CACHE_CAPACITY,
898 };
899 let xet_config = XetConfig::new();
900 let cache = DiskCache::initialize(&xet_config, &config).unwrap();
901
902 let key = random_key(&mut rng);
903 let range = ChunkRange::new(0, 4);
905 let (chunk_byte_indices, data) = random_bytes(&mut rng, &range, RANGE_LEN);
906 let put_result = cache.put(&key, &range, &chunk_byte_indices, data.as_slice()).await;
907 assert!(put_result.is_ok(), "{put_result:?}");
908
909 print_directory_contents(cache_root.as_ref());
910
911 for start in range.start..range.end {
912 for end in (start + 1)..=range.end {
913 let sub_range = ChunkRange::new(start, end);
914 let get_result = cache.get(&key, &sub_range).await.unwrap();
915 assert!(get_result.is_some(), "range: [{start} {end})");
916 let cache_range = get_result.unwrap();
917 assert_eq!(cache_range.range, sub_range);
918 assert_eq!(cache_range.offsets.len() as u32, sub_range.end - sub_range.start + 1);
920
921 for (expected, actual) in chunk_byte_indices[(start as usize)..=(end as usize)]
922 .iter()
923 .map(|v| *v - chunk_byte_indices[start as usize])
924 .zip(cache_range.offsets.iter())
925 {
926 assert_eq!(*actual, expected);
927 }
928
929 let start_byte = chunk_byte_indices[sub_range.start as usize] as usize;
930 let end_byte = chunk_byte_indices[sub_range.end as usize] as usize;
931 let data_portion = &data[start_byte..end_byte];
932 assert_eq!(data_portion, &cache_range.data);
933 }
934 }
935 }
936
937 #[tokio::test]
938 async fn test_puts_eviction() {
939 const MIN_NUM_KEYS: u32 = 12;
940 const CAP: u64 = (RANGE_LEN * (MIN_NUM_KEYS - 1)) as u64;
941 let cache_root = TempDir::new().unwrap();
942 let config = CacheConfig {
943 cache_directory: cache_root.path().to_path_buf(),
944 cache_size: CAP,
945 };
946 let xet_config = XetConfig::new();
947 let cache = DiskCache::initialize(&xet_config, &config).unwrap();
948 let mut it = RandomEntryIterator::std_from_seed(RANDOM_SEED);
949
950 for _ in 0..MIN_NUM_KEYS {
952 let (key, range, offsets, data) = it.next().unwrap();
953 assert!(cache.put(&key, &range, &offsets, &data).await.is_ok());
954 }
955 let total_bytes = cache.total_bytes().await;
956 assert!(total_bytes <= CAP, "cache size: {} <= {}", output_bytes(total_bytes), output_bytes(CAP));
957
958 let (key, range, offsets, data) = it.next().unwrap();
959 let result = cache.put(&key, &range, &offsets, &data).await;
960 assert!(result.is_ok());
961 }
962
963 #[tokio::test]
964 async fn test_same_puts_noop() {
965 let cache_root = TempDir::new().unwrap();
966 let config = CacheConfig {
967 cache_directory: cache_root.path().to_path_buf(),
968 cache_size: DEFAULT_CHUNK_CACHE_CAPACITY,
969 };
970 let xet_config = XetConfig::new();
971 let cache = DiskCache::initialize(&xet_config, &config).unwrap();
972 let mut it = RandomEntryIterator::std_from_seed(RANDOM_SEED).with_range_len(1000);
973 let (key, range, offsets, data) = it.next().unwrap();
974 assert!(cache.put(&key, &range, &offsets, &data).await.is_ok());
975 assert!(cache.put(&key, &range, &offsets, &data).await.is_ok());
976 }
977
978 #[tokio::test]
979 async fn test_overlap_range_data_mismatch_fail() {
980 let setup = || async move {
981 let mut it = RandomEntryIterator::std_from_seed(RANDOM_SEED);
982 let cache_root = TempDir::new().unwrap();
983 let config = CacheConfig {
984 cache_directory: cache_root.path().to_path_buf(),
985 cache_size: DEFAULT_CHUNK_CACHE_CAPACITY,
986 };
987 let xet_config = XetConfig::new();
988 let cache = DiskCache::initialize(&xet_config, &config).unwrap();
989 let (key, range, offsets, data) = it.next().unwrap();
990 assert!(cache.put(&key, &range, &offsets, &data).await.is_ok());
991 (cache_root, cache, key, range, offsets, data)
992 };
993
994 let (_cache_root, cache, key, range, mut offsets, data) = setup().await;
997 offsets.remove(1);
998 assert!(cache.put(&key, &range, &offsets, &data).await.is_err());
999
1000 let (_cache_root, cache, key, range, mut offsets, data) = setup().await;
1002 offsets[0] = 100;
1003 assert!(cache.put(&key, &range, &offsets, &data).await.is_err());
1004
1005 let (_cache_root, cache, key, range, mut offsets, data) = setup().await;
1007 *offsets.last_mut().unwrap() = data.len() as u32 + 1;
1008 assert!(cache.put(&key, &range, &offsets, &data).await.is_err());
1009
1010 let (_cache_root, cache, key, range, mut offsets, data) = setup().await;
1012 offsets[2] = offsets[1];
1013 assert!(cache.put(&key, &range, &offsets, &data).await.is_err());
1014
1015 let (_cache_root, cache, key, range, mut offsets, data) = setup().await;
1017 offsets[1] += 1;
1018 assert!(cache.put(&key, &range, &offsets, &data).await.is_err());
1019
1020 let (_cache_root, cache, key, range, offsets, data) = setup().await;
1023 assert!(cache.put(&key, &range, &offsets, &data[1..]).await.is_err());
1024
1025 let (_cache_root, cache, key, range, offsets, mut data) = setup().await;
1027 data[0] += 1;
1028 assert!(cache.put(&key, &range, &offsets, &data).await.is_err());
1029 }
1030
1031 #[tokio::test]
1032 async fn test_initialize_non_empty() {
1033 let cache_root = TempDir::new().unwrap();
1034 let config = CacheConfig {
1035 cache_directory: cache_root.path().to_path_buf(),
1036 cache_size: DEFAULT_CHUNK_CACHE_CAPACITY,
1037 };
1038 let xet_config = XetConfig::new();
1039 let cache = DiskCache::initialize(&xet_config, &config).unwrap();
1040
1041 let mut it = RandomEntryIterator::std_from_seed(RANDOM_SEED);
1042
1043 let mut keys_and_ranges = Vec::new();
1044
1045 for _ in 0..20 {
1046 let (key, range, offsets, data) = it.next().unwrap();
1047 assert!(cache.put(&key, &range, &offsets, &data).await.is_ok());
1048 keys_and_ranges.push((key, range));
1049 }
1050
1051 let cache2 = DiskCache::initialize(&xet_config, &config).unwrap();
1052 for (i, (key, range)) in keys_and_ranges.iter().enumerate() {
1053 let get_result = cache2.get(&key, &range).await;
1054 assert!(get_result.is_ok(), "{i} {get_result:?}");
1055 assert!(get_result.unwrap().is_some(), "{i}");
1056 }
1057
1058 let cache_keys = cache.state.read().await.inner.keys().cloned().collect::<BTreeSet<_>>();
1059 let cache2_keys = cache2.state.read().await.inner.keys().cloned().collect::<BTreeSet<_>>();
1060 assert_eq!(cache_keys, cache2_keys);
1061 }
1062
1063 #[tokio::test]
1064 async fn test_initialize_too_large_file() {
1065 const LARGE_FILE: u64 = 1000;
1066 let cache_root = TempDir::new().unwrap();
1067 let config = CacheConfig {
1068 cache_directory: cache_root.path().to_path_buf(),
1069 cache_size: DEFAULT_CHUNK_CACHE_CAPACITY,
1070 };
1071 let xet_config = XetConfig::new();
1072 let cache = DiskCache::initialize(&xet_config, &config).unwrap();
1073 let mut it = RandomEntryIterator::std_from_seed(RANDOM_SEED).with_range_len(LARGE_FILE as u32);
1074
1075 let (key, range, offsets, data) = it.next().unwrap();
1076 cache.put(&key, &range, &offsets, &data).await.unwrap();
1077 let config = CacheConfig {
1078 cache_directory: cache_root.path().to_path_buf(),
1079 cache_size: LARGE_FILE - 1,
1080 };
1081 let cache2 = DiskCache::initialize(&xet_config, &config).unwrap();
1082
1083 assert_eq!(cache2.total_bytes().await, 0);
1084 }
1085
1086 #[tokio::test]
1087 async fn test_initialize_stops_loading_early_with_too_many_files() {
1088 const LARGE_FILE: u64 = 1000;
1089 let cache_root = TempDir::new().unwrap();
1090 let xet_config = XetConfig::new();
1091 let config = CacheConfig {
1092 cache_directory: cache_root.path().to_path_buf(),
1093 cache_size: LARGE_FILE * 10,
1094 };
1095 let cache = DiskCache::initialize(&xet_config, &config).unwrap();
1096 let mut it = RandomEntryIterator::std_from_seed(RANDOM_SEED).with_range_len(LARGE_FILE as u32);
1097 for _ in 0..10 {
1098 let (key, range, offsets, data) = it.next().unwrap();
1099 cache.put(&key, &range, &offsets, &data).await.unwrap();
1100 }
1101
1102 let cap2 = LARGE_FILE * 2;
1103 let config = CacheConfig {
1104 cache_directory: cache_root.path().to_path_buf(),
1105 cache_size: cap2,
1106 };
1107 let cache2 = DiskCache::initialize(&xet_config, &config).unwrap();
1108
1109 assert!(cache2.total_bytes().await < cap2 * 3, "{} < {}", cache2.total_bytes().await, cap2 * 3);
1110 }
1111
1112 #[test]
1113 fn test_dir_name_to_key() {
1114 let s = "oL-Xqk1J00kVe1U4kCko-Kw4zaVv3-4U73i27w5DViBkZWZhdWx0";
1115 let key = try_parse_key(s.as_bytes());
1116 assert!(key.is_ok(), "{key:?}")
1117 }
1118
1119 #[tokio::test]
1120 async fn test_unknown_eviction() {
1121 let cache_root = TempDir::new().unwrap();
1122 let capacity = 12 * RANGE_LEN as u64;
1123 let xet_config = XetConfig::new();
1124 let config = CacheConfig {
1125 cache_directory: cache_root.path().to_path_buf(),
1126 cache_size: capacity,
1127 };
1128 let cache = DiskCache::initialize(&xet_config, &config).unwrap();
1129 let mut it = RandomEntryIterator::std_from_seed(RANDOM_SEED);
1130 let (key, range, chunk_byte_indices, data) = it.next().unwrap();
1131 cache.put(&key, &range, &chunk_byte_indices, &data).await.unwrap();
1132
1133 let cache2 = DiskCache::initialize(&xet_config, &config).unwrap();
1134 let get_result = cache2.get(&key, &range).await;
1135 assert!(get_result.is_ok());
1136 assert!(get_result.unwrap().is_some());
1137
1138 let (key2, range2, chunk_byte_indices2, data2) = it.next().unwrap();
1139 assert!(cache2.put(&key2, &range2, &chunk_byte_indices2, &data2).await.is_ok());
1140
1141 let mut get_result_1 = cache2.get(&key, &range).await.unwrap();
1142 let mut i = 0;
1143 while get_result_1.is_some() && i < 50 {
1144 i += 1;
1145 let (key2, range2, chunk_byte_indices2, data2) = it.next().unwrap();
1146 cache2.put(&key2, &range2, &chunk_byte_indices2, &data2).await.unwrap();
1147 get_result_1 = cache2.get(&key, &range).await.unwrap();
1148 }
1149 if get_result_1.is_some() {
1150 return;
1152 }
1153 let get_result_post_eviction = cache.get(&key, &range).await;
1156 assert!(get_result_post_eviction.is_ok());
1157 assert!(get_result_post_eviction.unwrap().is_none());
1158 }
1159
1160 #[tokio::test]
1161 async fn put_subrange() {
1162 let cache_root = TempDir::new().unwrap();
1163 let config = CacheConfig {
1164 cache_directory: cache_root.path().to_path_buf(),
1165 cache_size: DEFAULT_CHUNK_CACHE_CAPACITY,
1166 };
1167 let xet_config = XetConfig::new();
1168 let cache = DiskCache::initialize(&xet_config, &config).unwrap();
1169
1170 let (key, range, chunk_byte_indices, data) = RandomEntryIterator::std_from_seed(RANDOM_SEED).next().unwrap();
1171 cache.put(&key, &range, &chunk_byte_indices, &data).await.unwrap();
1172 let total_bytes = cache.total_bytes().await;
1173
1174 let left_range = ChunkRange::new(range.start, range.end - 1);
1176 let left_chunk_byte_indices = &chunk_byte_indices[..chunk_byte_indices.len() - 1];
1177 let left_data = &data[..*left_chunk_byte_indices.last().unwrap() as usize];
1178 assert!(cache.put(&key, &left_range, left_chunk_byte_indices, left_data).await.is_ok());
1179 assert_eq!(total_bytes, cache.total_bytes().await);
1180
1181 let right_range = ChunkRange::new(range.start + 1, range.end);
1183 let right_chunk_byte_indices: Vec<u32> =
1184 (&chunk_byte_indices[1..]).iter().map(|v| v - chunk_byte_indices[1]).collect();
1185 let right_data = &data[chunk_byte_indices[1] as usize..];
1186 assert!(
1187 cache
1188 .put(&key, &right_range, &right_chunk_byte_indices, right_data)
1189 .await
1190 .is_ok()
1191 );
1192 assert_eq!(total_bytes, cache.total_bytes().await);
1193
1194 let middle_range = ChunkRange::new(range.start + 1, range.end - 1);
1196 let middle_chunk_byte_indices: Vec<u32> = (&chunk_byte_indices[1..(chunk_byte_indices.len() - 1)])
1197 .iter()
1198 .map(|v| v - chunk_byte_indices[1])
1199 .collect();
1200 let middle_data =
1201 &data[chunk_byte_indices[1] as usize..chunk_byte_indices[chunk_byte_indices.len() - 2] as usize];
1202
1203 assert!(
1204 cache
1205 .put(&key, &middle_range, &middle_chunk_byte_indices, middle_data)
1206 .await
1207 .is_ok()
1208 );
1209 assert_eq!(total_bytes, cache.total_bytes().await);
1210 }
1211
1212 #[tokio::test]
1213 async fn test_evictions_with_multiple_range_per_key() {
1214 const NUM: u32 = 12;
1215 let cache_root = TempDir::new().unwrap();
1216 let capacity = (NUM * RANGE_LEN) as u64;
1217 let xet_config = XetConfig::new();
1218 let config = CacheConfig {
1219 cache_directory: cache_root.path().to_path_buf(),
1220 cache_size: capacity,
1221 };
1222 let cache = DiskCache::initialize(&xet_config, &config).unwrap();
1223 let mut it = RandomEntryIterator::std_from_seed(RANDOM_SEED).with_one_chunk_ranges(true);
1224 let (key, _, _, _) = it.next().unwrap();
1225 let mut previously_put: Vec<(Key, ChunkRange)> = Vec::new();
1226
1227 for _ in 0..(NUM / 2) {
1228 let (key2, mut range, chunk_byte_indices, data) = it.next().unwrap();
1229 while previously_put.iter().any(|(_, r)| r.start == range.start) {
1230 range.start += 1 % 1000;
1231 }
1232 cache.put(&key, &range, &chunk_byte_indices, &data).await.unwrap();
1233 previously_put.push((key.clone(), range.clone()));
1234 cache.put(&key2, &range, &chunk_byte_indices, &data).await.unwrap();
1235 previously_put.push((key2, range));
1236 }
1237
1238 let mut num_hits = 0;
1239 for (key, range) in &previously_put {
1240 let result = cache.get(key, range).await;
1241 assert!(result.is_ok());
1242 let result = result.unwrap();
1243 if result.is_some() {
1244 num_hits += 1;
1245 }
1246 }
1247 assert_ne!(num_hits, 0);
1249
1250 assert!(cache.state.read().await.inner.contains_key(&key), "evicted key that should have remained in cache");
1252 }
1253
1254 #[test]
1255 fn test_initialize_with_cache_size_0() {
1256 let xet_config = XetConfig::new();
1257 assert!(
1258 DiskCache::initialize(
1259 &xet_config,
1260 &CacheConfig {
1261 cache_directory: "/tmp".into(),
1262 cache_size: 0,
1263 },
1264 )
1265 .is_err()
1266 );
1267 }
1268}
1269
1270#[cfg(test)]
1271mod concurrency_tests {
1272 use tempfile::TempDir;
1273 use xet_runtime::config::XetConfig;
1274
1275 use super::super::{CacheConfig, ChunkCache};
1276 use super::DiskCache;
1277 use super::test_utils::{RANGE_LEN, RandomEntryIterator};
1278
1279 const NUM_ITEMS_PER_TASK: usize = 20;
1280 const RANDOM_SEED: u64 = 878987298749287;
1281
1282 const DEFAULT_CHUNK_CACHE_CAPACITY: u64 = 10_000_000_000;
1283
1284 #[tokio::test]
1285 async fn test_run_concurrently() {
1286 let cache_root = TempDir::new().unwrap();
1287
1288 let config = CacheConfig {
1289 cache_directory: cache_root.path().to_path_buf(),
1290 cache_size: DEFAULT_CHUNK_CACHE_CAPACITY,
1291 };
1292 let xet_config = XetConfig::new();
1293 let cache = DiskCache::initialize(&xet_config, &config).unwrap();
1294
1295 let num_tasks = 2 + rand::random::<u8>() % 14;
1296
1297 let mut handles = Vec::with_capacity(num_tasks as usize);
1298 for _ in 0..num_tasks {
1299 let cache_clone = cache.clone();
1300 handles.push(tokio::spawn(async move {
1301 let mut it = RandomEntryIterator::std_from_seed(RANDOM_SEED);
1302 let mut kr = Vec::with_capacity(NUM_ITEMS_PER_TASK);
1303 for _ in 0..NUM_ITEMS_PER_TASK {
1304 let (key, range, chunk_byte_indices, data) = it.next().unwrap();
1305 assert!(cache_clone.put(&key, &range, &chunk_byte_indices, &data).await.is_ok());
1306 kr.push((key, range));
1307 }
1308 for (key, range) in kr {
1309 assert!(cache_clone.get(&key, &range).await.is_ok());
1310 }
1311 }))
1312 }
1313
1314 for handle in handles {
1315 handle.await.expect("join should not error");
1316 }
1317 }
1318
1319 #[tokio::test]
1320 #[cfg_attr(feature = "smoke-test", ignore)]
1321 async fn test_run_concurrently_with_evictions() {
1322 let cache_root = TempDir::new().unwrap();
1323 let xet_config = XetConfig::new();
1324 let config = CacheConfig {
1325 cache_directory: cache_root.path().to_path_buf(),
1326 cache_size: RANGE_LEN as u64 * NUM_ITEMS_PER_TASK as u64,
1327 };
1328 let cache = DiskCache::initialize(&xet_config, &config).unwrap();
1329
1330 let num_tasks = 2 + rand::random::<u8>() % 14;
1331
1332 let mut handles = Vec::with_capacity(num_tasks as usize);
1333 for _ in 0..num_tasks {
1334 let cache_clone = cache.clone();
1335 handles.push(tokio::spawn(async move {
1336 let mut it = RandomEntryIterator::std_from_seed(RANDOM_SEED);
1337 let mut kr = Vec::with_capacity(NUM_ITEMS_PER_TASK);
1338 for _ in 0..NUM_ITEMS_PER_TASK {
1339 let (key, range, chunk_byte_indices, data) = it.next().unwrap();
1340 assert!(cache_clone.put(&key, &range, &chunk_byte_indices, &data).await.is_ok());
1341 kr.push((key, range));
1342 }
1343 for (key, range) in kr {
1344 assert!(cache_clone.get(&key, &range).await.is_ok());
1345 }
1346 }))
1347 }
1348
1349 for handle in handles {
1350 handle.await.expect("join should not error");
1351 }
1352 }
1353
1354 #[tokio::test(flavor = "multi_thread")]
1355 async fn test_run_concurrently_thundering_herd() {
1356 let cache_root = TempDir::new().unwrap();
1357 let xet_config = XetConfig::new();
1358 let config = CacheConfig {
1359 cache_directory: cache_root.path().to_path_buf(),
1360 cache_size: RANGE_LEN as u64 * NUM_ITEMS_PER_TASK as u64,
1361 };
1362 let cache = DiskCache::initialize(&xet_config, &config).unwrap();
1363
1364 let mut it = RandomEntryIterator::std_from_seed(RANDOM_SEED);
1366 let (key, range, chunk_byte_indices, data) = it.next().unwrap();
1367
1368 let num_tasks = 64;
1370 let mut handles = Vec::with_capacity(num_tasks as usize);
1371 for _ in 0..num_tasks {
1372 let cache_clone = cache.clone();
1373 let key = key.clone();
1374 let range = range.clone();
1375 let chunk_byte_indices = chunk_byte_indices.clone();
1376 let data_clone = data.clone();
1377 handles.push(tokio::spawn(async move {
1378 let res = cache_clone.put(&key, &range, &chunk_byte_indices, &data_clone).await;
1379 assert!(res.is_ok(), "err: {res:?}");
1380 }))
1381 }
1382
1383 for handle in handles {
1384 handle.await.expect("join should not error");
1385 }
1386
1387 let state = cache.state.read().await;
1389 let items = state.inner.get(&key).unwrap();
1390
1391 let num = items.iter().filter(|item| item.range == range).count();
1392 assert_eq!(num, 1);
1393 }
1394}