Skip to main content

xet_client/chunk_cache/
disk.rs

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
30// consistently use URL_SAFE (also file path safe) base64 codec
31pub(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        // attempt to find a matching range in the given key's items using
56        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    /// removed items from the cache (including deleting from file system)
65    /// until at least to_remove number of bytes have been removed
66    ///
67    /// removes data from in memory state and returns a list of file paths to delete
68    /// (so that deletion can occur after the locked state is dropped)
69    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    /// returns the key and index within that key for a random item
104    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        // should never occur
124        error!("cache random_item for eviction: tried to return random item error not enough items");
125        None
126    }
127}
128
129/// DiskCache is a ChunkCache implementor that saves data on the file system
130#[derive(Debug, Clone)]
131pub struct DiskCache {
132    cache_root: PathBuf,
133    capacity: u64,
134    state: Arc<RwLock<CacheState>>,
135}
136
137// helper for analysis binary to print inner state
138#[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    /// initialize will create a new DiskCache with the capacity and cache root based on the config
181    /// the cache file system layout is rooted at the provided config.cache_directory and initialize
182    /// will attempt to load any pre-existing cache state into memory.
183    ///
184    /// an configured size of 0 caused initialization to fail
185    ///
186    /// The cache layout is as follows:
187    ///
188    /// each key (cas hash) in the cache is a directory, containing "cache items" that each provide
189    /// some range of data.
190    ///
191    /// keys are grouped into subdirectories under the cache rootbased on the first 2 chacters of their
192    /// file name, which is base64 encoded, leading to at most 64 * 64 directories under the cache root.
193    ///
194    /// cache_root/
195    /// ├── [ab]/
196    /// │   ├── [key 1 (ab123...)]/
197    /// │   │   ├── [range 0-100, file_len, file_hash]
198    /// │   │   ├── [range 102-300, file_len, file_hash]
199    /// │   │   └── [range 900-1024, file_len, file_hash]
200    /// │   ├── [key 2 (ab456...)]/
201    /// │       └── [range 0-1020, file_len, file_hash]
202    /// ├── [cd]/
203    /// │   └── [key 3 (cd123...)]/
204    /// │       ├── [range 30-31, file_len, file_hash]
205    /// │       ├── [range 400-402, file_len, file_hash]
206    /// │       ├── [range 404-405, file_len, file_hash]
207    /// │       └── [range 679-700, file_len, file_hash]
208    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        // May take a while; don't block the runtime for this.
216        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        // loop through cache root directory, first level containing "prefix" directories
240        // each of which may contain key directories with cache items
241        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            // loop through key directories inside prefix directory
257            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                // asserts that the prefix dir name is actually the prefix of this key dir
267                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                // loop through cache items inside key directory
290                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 already filled capacity, stop iterating over cache items
302                    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 is guaranteed to be more than 1 element at this point
377            || 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        // check if we already contain the range
385        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            // refusing to add this item as it is too large for the cache with configured capacity
397            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        // write cache item file
414        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        // evict items after ensuring the file write but before committing to cache state
420        // to avoid removing new item.
421        let mut state_write = self.state.write().await;
422
423        // acquiring lock to state before closing the file
424        // this will ensure that this thread is the only one writing to the final
425        // cache file but allowing other threads to modify the state while we write the file
426        // before committing it.
427        if state_write.find_match(key, range).is_some() {
428            // another thread already added this item or overlapping item while this thread
429            // was writing the file
430            fw.abort()?;
431            return Ok(());
432        }
433        fw.close()?;
434
435        // Evict entries to make sure we have enough room.
436        let evicted_paths = state_write.evict_to_capacity(self.capacity - cache_item.len)?;
437
438        // add the item info in-memory state after evictions are done
439        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        // release lock
445        drop(state_write);
446
447        // remove files after done with modifying in memory state and releasing lock
448        for (key, cache_item) in evicted_paths {
449            let path = self.item_path(&key, &cache_item)?;
450            remove_file(&path)?;
451            // check and try to remove key path if all items evicted for key
452            let dir_path = path.parent().ok_or(ChunkCacheError::Infallible)?;
453            check_remove_dir(dir_path)?;
454        }
455
456        Ok(())
457    }
458
459    // on a non-error case, returns true if the item is a good match and a new item should not be inserted
460    // returns false if not a good match and should be removed.
461    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        // this is a redundant check
470        if range.start < cache_item.range.start || range.end > cache_item.range.end {
471            return Err(ChunkCacheError::BadRange);
472        }
473
474        // validate stored data
475        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        // validate the chunk_byte_indices and data input against stored data
500        // the chunk_byte_indices should match the chunk lengths, if the ranges
501        // don't start at the same chunk, values will be different, what's important
502        // to match is the chunk lengths, i.e. difference in the offsets.
503        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    /// removes an item from both the in-memory state of the cache and the file system
528    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                    // item is no longer in the state
535                    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
622// wrapper over std::fs::read_dir
623// returns Ok(None) on a not found error
624fn 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
637// returns Ok(Some(_)) if result dirent is a directory, Ok(None) if was removed
638// also returns an Ok(None) if the dirent is not a directory, in which case we should
639//   not remove it in case the user put something inadvertently or intentionally,
640//   but not attempt to parse it as a valid cache directory.
641// Err(_) if an unrecoverable error occurred
642fn 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
668// given a result from readdir attempts to parse it as a cache file handle
669// i.e. validate its file name against the contents (excluding file-hash-validation)
670// validate that it is a file, correct len, and is not too large.
671fn 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    // don't track an item that takes up the whole capacity
706    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        // file is invalid, remove it
722        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
734/// removes a file but disregards a "NotFound" error if the file is already gone
735fn 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
744/// removes a directory but disregards a "NotFound" error if the directory is already gone
745fn 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
754// assumes dir_path is a path to a key directory i.e. cache_root/<prefix_dir>/<key_dir>
755// assumes a misformatted path is an error
756// checks if the directory is empty and removes it if so, then checks if the prefix dir is empty and removes it if so
757fn 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    // directory empty, remove it
766    remove_dir(&dir_path)?;
767
768    // try to check and remove the prefix dir
769    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    // directory empty, remove it
779    remove_dir(prefix_dir)
780}
781
782/// tries to parse just a Key from a file name encoded by fn `key_dir`
783/// expects only the key portion of the file path, with the prefix not present.
784fn 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
791/// key_dir returns a directory name string formed from the key
792/// the format is BASE64_encode([ key.hash[..], key.prefix.as_bytes()[..] ])
793fn 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        // hit
879        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        // miss
888        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        // following parts of test assume overall inserted range includes chunk 0
904        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 that offsets has 1 more item than the range len difference
919                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        // fill the cache to almost capacity
951        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        // bad offsets
995        // totally random, mismatch len from range
996        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        // start isn't 0
1001        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        // end isn't data.len()
1006        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        // not strictly increasing
1011        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        // not matching
1016        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        // bad data
1021        // size mismatch given offsets
1022        let (_cache_root, cache, key, range, offsets, data) = setup().await;
1023        assert!(cache.put(&key, &range, &offsets, &data[1..]).await.is_err());
1024
1025        // data changed
1026        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            // randomness didn't evict the record after 50 tries, don't test this case now
1151            return;
1152        }
1153        // we've evicted the original record from the cache
1154        // note using the original cache handle without updates!
1155        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        // left range
1175        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        // right range
1182        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        // middle range
1195        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 got some hits, exact number depends on item size
1248        assert_ne!(num_hits, 0);
1249
1250        // assert that we haven't evicted all keys for key with multiple items
1251        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        // data inserted is the same
1365        let mut it = RandomEntryIterator::std_from_seed(RANDOM_SEED);
1366        let (key, range, chunk_byte_indices, data) = it.next().unwrap();
1367
1368        // Spawn tasks to simultaneously insert into cache
1369        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        // check that there is only 1 term in the cache for this data
1388        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}