Skip to main content

hermes_core/directories/
slice_cache.rs

1//! Slice-level caching directory with overlap management
2//!
3//! Caches byte ranges from files, merging overlapping ranges and
4//! evicting least-recently-used slices when the cache limit is reached.
5
6use async_trait::async_trait;
7use parking_lot::RwLock;
8use std::collections::BTreeMap;
9use std::io::{self, Read, Write};
10use std::ops::Range;
11use std::path::{Path, PathBuf};
12use std::sync::Arc;
13
14use super::{Directory, FileHandle, OwnedBytes, RangeReadFn};
15
16/// File extension for slice cache files
17pub const SLICE_CACHE_EXTENSION: &str = "slicecache";
18
19/// Magic bytes for slice cache file format
20const SLICE_CACHE_MAGIC: &[u8; 8] = b"HRMSCACH";
21
22/// Current version of the slice cache format
23/// v2: Added file size caching
24const SLICE_CACHE_VERSION: u32 = 2;
25
26/// A cached slice of a file
27#[derive(Debug, Clone)]
28struct CachedSlice {
29    /// Byte range in the file
30    range: Range<u64>,
31    /// The cached data
32    data: Arc<Vec<u8>>,
33    /// Access counter for LRU eviction
34    access_count: u64,
35}
36
37/// Per-file slice cache using interval tree for overlap detection
38struct FileSliceCache {
39    /// Slices sorted by start offset for efficient overlap detection
40    slices: BTreeMap<u64, CachedSlice>,
41    /// Total bytes cached for this file
42    total_bytes: usize,
43}
44
45impl FileSliceCache {
46    fn new() -> Self {
47        Self {
48            slices: BTreeMap::new(),
49            total_bytes: 0,
50        }
51    }
52
53    /// Serialize this file cache to bytes
54    fn serialize(&self) -> Vec<u8> {
55        let mut buf = Vec::new();
56        // Number of slices
57        buf.extend_from_slice(&(self.slices.len() as u32).to_le_bytes());
58        for slice in self.slices.values() {
59            // Range start and end
60            buf.extend_from_slice(&slice.range.start.to_le_bytes());
61            buf.extend_from_slice(&slice.range.end.to_le_bytes());
62            // Data length and data
63            buf.extend_from_slice(&(slice.data.len() as u32).to_le_bytes());
64            buf.extend_from_slice(&slice.data);
65        }
66        buf
67    }
68
69    /// Deserialize from bytes, returns (cache, bytes_consumed)
70    fn deserialize(data: &[u8], access_counter: u64) -> io::Result<(Self, usize)> {
71        let mut pos = 0;
72        if data.len() < 4 {
73            return Err(io::Error::new(
74                io::ErrorKind::InvalidData,
75                "truncated slice cache",
76            ));
77        }
78        let num_slices = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
79        pos += 4;
80
81        let mut cache = FileSliceCache::new();
82        for _ in 0..num_slices {
83            if pos + 20 > data.len() {
84                return Err(io::Error::new(
85                    io::ErrorKind::InvalidData,
86                    "truncated slice entry",
87                ));
88            }
89            let range_start = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
90            pos += 8;
91            let range_end = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
92            pos += 8;
93            let data_len = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
94            pos += 4;
95
96            if pos + data_len > data.len() {
97                return Err(io::Error::new(
98                    io::ErrorKind::InvalidData,
99                    "truncated slice data",
100                ));
101            }
102            let slice_data = data[pos..pos + data_len].to_vec();
103            pos += data_len;
104
105            cache.total_bytes += slice_data.len();
106            cache.slices.insert(
107                range_start,
108                CachedSlice {
109                    range: range_start..range_end,
110                    data: Arc::new(slice_data),
111                    access_count: access_counter,
112                },
113            );
114        }
115        Ok((cache, pos))
116    }
117
118    /// Get iterator over all slices for serialization
119    #[allow(dead_code)]
120    fn iter_slices(&self) -> impl Iterator<Item = (&u64, &CachedSlice)> {
121        self.slices.iter()
122    }
123
124    /// Try to read from cache, returns None if not fully cached
125    fn try_read(&mut self, range: Range<u64>, access_counter: &mut u64) -> Option<Vec<u8>> {
126        // Find slices that might contain our range
127        let start = range.start;
128        let end = range.end;
129
130        // Look for a slice that contains the entire range
131        let mut found_key = None;
132        for (&slice_start, slice) in self.slices.range(..=start).rev() {
133            if slice_start <= start && slice.range.end >= end {
134                found_key = Some((
135                    slice_start,
136                    (start - slice_start) as usize,
137                    (end - start) as usize,
138                ));
139                break;
140            }
141        }
142
143        if let Some((key, offset, len)) = found_key {
144            // Update access count for LRU
145            *access_counter += 1;
146            if let Some(s) = self.slices.get_mut(&key) {
147                s.access_count = *access_counter;
148                return Some(s.data[offset..offset + len].to_vec());
149            }
150        }
151
152        None
153    }
154
155    /// Insert a slice, merging with overlapping slices
156    /// Returns the net change in bytes (can be negative if merge reduces size, but typically positive)
157    fn insert(&mut self, range: Range<u64>, data: Vec<u8>, access_counter: u64) -> isize {
158        let start = range.start;
159        let end = range.end;
160        let data_len = data.len();
161
162        // Find and remove overlapping slices
163        let mut to_remove = Vec::new();
164        let mut merged_start = start;
165        let mut merged_end = end;
166        let mut merged_data: Option<Vec<u8>> = None;
167        let mut bytes_removed: usize = 0;
168
169        for (&slice_start, slice) in &self.slices {
170            // Check for overlap
171            if slice_start < end && slice.range.end > start {
172                to_remove.push(slice_start);
173
174                // Extend merged range
175                merged_start = merged_start.min(slice_start);
176                merged_end = merged_end.max(slice.range.end);
177            }
178        }
179
180        // If we have overlaps, merge the data
181        if !to_remove.is_empty() {
182            let merged_len = (merged_end - merged_start) as usize;
183            let mut new_data = vec![0u8; merged_len];
184
185            // Copy existing slices
186            for &slice_start in &to_remove {
187                if let Some(slice) = self.slices.get(&slice_start) {
188                    let offset = (slice_start - merged_start) as usize;
189                    new_data[offset..offset + slice.data.len()].copy_from_slice(&slice.data);
190                    bytes_removed += slice.data.len();
191                    self.total_bytes -= slice.data.len();
192                }
193            }
194
195            // Copy new data (overwrites any overlapping parts)
196            let offset = (start - merged_start) as usize;
197            new_data[offset..offset + data_len].copy_from_slice(&data);
198
199            // Remove old slices
200            for slice_start in to_remove {
201                self.slices.remove(&slice_start);
202            }
203
204            merged_data = Some(new_data);
205        }
206
207        // Insert the (possibly merged) slice
208        let (final_start, final_data) = if let Some(md) = merged_data {
209            (merged_start, md)
210        } else {
211            (start, data)
212        };
213
214        let bytes_added = final_data.len();
215        self.total_bytes += bytes_added;
216
217        self.slices.insert(
218            final_start,
219            CachedSlice {
220                range: final_start..final_start + bytes_added as u64,
221                data: Arc::new(final_data),
222                access_count: access_counter,
223            },
224        );
225
226        // Return net change: bytes added minus bytes removed during merge
227        bytes_added as isize - bytes_removed as isize
228    }
229
230    /// Evict least recently used slices to free up space
231    fn evict_lru(&mut self, bytes_to_free: usize) -> usize {
232        let mut freed = 0;
233
234        while freed < bytes_to_free && !self.slices.is_empty() {
235            // Find the slice with lowest access count
236            let lru_key = self
237                .slices
238                .iter()
239                .min_by_key(|(_, s)| s.access_count)
240                .map(|(&k, _)| k);
241
242            if let Some(key) = lru_key {
243                if let Some(slice) = self.slices.remove(&key) {
244                    freed += slice.data.len();
245                    self.total_bytes -= slice.data.len();
246                }
247            } else {
248                break;
249            }
250        }
251
252        freed
253    }
254}
255
256/// Slice-caching directory wrapper
257///
258/// Caches byte ranges from the inner directory, with:
259/// - Overlap detection and merging
260/// - LRU eviction when cache limit is reached
261/// - Bounded total memory usage
262/// - File size caching to avoid HEAD requests
263pub struct SliceCachingDirectory<D: Directory> {
264    inner: Arc<D>,
265    /// Per-file slice caches
266    caches: Arc<RwLock<std::collections::HashMap<PathBuf, FileSliceCache>>>,
267    /// Cached file sizes (avoids HEAD requests on lazy open)
268    file_sizes: Arc<RwLock<std::collections::HashMap<PathBuf, u64>>>,
269    /// Maximum total bytes to cache
270    max_bytes: usize,
271    /// Current total bytes cached
272    current_bytes: Arc<RwLock<usize>>,
273    /// Global access counter for LRU
274    access_counter: Arc<RwLock<u64>>,
275    /// Index name for Directory-layer metric labels (also forwarded to inner)
276    label: super::IndexLabel,
277}
278
279impl<D: Directory> SliceCachingDirectory<D> {
280    /// Create a new slice-caching directory with the given memory limit
281    pub fn new(inner: D, max_bytes: usize) -> Self {
282        Self {
283            inner: Arc::new(inner),
284            caches: Arc::new(RwLock::new(std::collections::HashMap::new())),
285            file_sizes: Arc::new(RwLock::new(std::collections::HashMap::new())),
286            max_bytes,
287            current_bytes: Arc::new(RwLock::new(0)),
288            access_counter: Arc::new(RwLock::new(0)),
289            label: super::IndexLabel::default(),
290        }
291    }
292
293    /// Get a reference to the inner directory
294    pub fn inner(&self) -> &D {
295        &self.inner
296    }
297
298    /// Try to read from cache
299    fn try_cache_read(&self, path: &Path, range: Range<u64>) -> Option<Vec<u8>> {
300        let mut caches = self.caches.write();
301        let mut counter = self.access_counter.write();
302
303        if let Some(file_cache) = caches.get_mut(path) {
304            file_cache.try_read(range, &mut counter)
305        } else {
306            None
307        }
308    }
309
310    /// Insert into cache, evicting if necessary
311    fn cache_insert(&self, path: &Path, range: Range<u64>, data: Vec<u8>) {
312        let data_len = data.len();
313
314        // Check if we need to evict
315        {
316            let current = *self.current_bytes.read();
317            if current + data_len > self.max_bytes {
318                self.evict_to_fit(data_len);
319            }
320        }
321
322        let mut caches = self.caches.write();
323        let counter = *self.access_counter.read();
324
325        let file_cache = caches
326            .entry(path.to_path_buf())
327            .or_insert_with(FileSliceCache::new);
328
329        let net_change = file_cache.insert(range, data, counter);
330        let mut current = self.current_bytes.write();
331        if net_change >= 0 {
332            *current += net_change as usize;
333        } else {
334            *current = current.saturating_sub((-net_change) as usize);
335        }
336    }
337
338    /// Evict slices to make room for new data
339    fn evict_to_fit(&self, needed: usize) {
340        let mut caches = self.caches.write();
341        let mut current = self.current_bytes.write();
342
343        let target = if *current + needed > self.max_bytes {
344            (*current + needed) - self.max_bytes
345        } else {
346            return;
347        };
348
349        let mut freed = 0;
350
351        // Evict from all file caches until we have enough space
352        while freed < target {
353            // Find the file cache with the oldest access
354            let oldest_file = caches
355                .iter()
356                .filter(|(_, fc)| !fc.slices.is_empty())
357                .min_by_key(|(_, fc)| {
358                    fc.slices
359                        .values()
360                        .map(|s| s.access_count)
361                        .min()
362                        .unwrap_or(u64::MAX)
363                })
364                .map(|(p, _)| p.clone());
365
366            if let Some(path) = oldest_file {
367                if let Some(file_cache) = caches.get_mut(&path) {
368                    freed += file_cache.evict_lru(target - freed);
369                }
370            } else {
371                break;
372            }
373        }
374
375        *current = current.saturating_sub(freed);
376    }
377
378    /// Get cache statistics
379    pub fn stats(&self) -> SliceCacheStats {
380        let caches = self.caches.read();
381        let mut total_slices = 0;
382        let mut files_cached = 0;
383
384        for fc in caches.values() {
385            if !fc.slices.is_empty() {
386                files_cached += 1;
387                total_slices += fc.slices.len();
388            }
389        }
390
391        SliceCacheStats {
392            total_bytes: *self.current_bytes.read(),
393            max_bytes: self.max_bytes,
394            total_slices,
395            files_cached,
396        }
397    }
398
399    /// Serialize the entire cache to a single binary blob
400    ///
401    /// Format (v2):
402    /// - Magic: 8 bytes "HRMSCACH"
403    /// - Version: 4 bytes (u32 LE)
404    /// - Num files: 4 bytes (u32 LE)
405    /// - For each file:
406    ///   - Path length: 4 bytes (u32 LE)
407    ///   - Path: UTF-8 bytes
408    ///   - File cache data (see FileSliceCache::serialize)
409    /// - Num file sizes: 4 bytes (u32 LE) [v2+]
410    /// - For each file size: [v2+]
411    ///   - Path length: 4 bytes (u32 LE)
412    ///   - Path: UTF-8 bytes
413    ///   - File size: 8 bytes (u64 LE)
414    pub fn serialize(&self) -> Vec<u8> {
415        let caches = self.caches.read();
416        let file_sizes = self.file_sizes.read();
417        let mut buf = Vec::new();
418
419        // Magic and version
420        buf.extend_from_slice(SLICE_CACHE_MAGIC);
421        buf.extend_from_slice(&SLICE_CACHE_VERSION.to_le_bytes());
422
423        // Count non-empty caches
424        let non_empty: Vec<_> = caches
425            .iter()
426            .filter(|(_, fc)| !fc.slices.is_empty())
427            .collect();
428        buf.extend_from_slice(&(non_empty.len() as u32).to_le_bytes());
429
430        for (path, file_cache) in non_empty {
431            // Path
432            let path_str = path.to_string_lossy();
433            let path_bytes = path_str.as_bytes();
434            buf.extend_from_slice(&(path_bytes.len() as u32).to_le_bytes());
435            buf.extend_from_slice(path_bytes);
436
437            // File cache data
438            let cache_data = file_cache.serialize();
439            buf.extend_from_slice(&cache_data);
440        }
441
442        // v2: File sizes section
443        buf.extend_from_slice(&(file_sizes.len() as u32).to_le_bytes());
444        for (path, &size) in file_sizes.iter() {
445            let path_str = path.to_string_lossy();
446            let path_bytes = path_str.as_bytes();
447            buf.extend_from_slice(&(path_bytes.len() as u32).to_le_bytes());
448            buf.extend_from_slice(path_bytes);
449            buf.extend_from_slice(&size.to_le_bytes());
450        }
451
452        buf
453    }
454
455    /// Deserialize and prefill the cache from a binary blob
456    ///
457    /// This loads cached slices from a previously serialized cache file.
458    /// Existing cache entries are preserved; new entries are merged in.
459    pub fn deserialize(&self, data: &[u8]) -> io::Result<()> {
460        let mut pos = 0;
461
462        // Check magic
463        if data.len() < 16 {
464            return Err(io::Error::new(
465                io::ErrorKind::InvalidData,
466                "slice cache too short",
467            ));
468        }
469        if &data[pos..pos + 8] != SLICE_CACHE_MAGIC {
470            return Err(io::Error::new(
471                io::ErrorKind::InvalidData,
472                "invalid slice cache magic",
473            ));
474        }
475        pos += 8;
476
477        // Check version (v2 only)
478        let version = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
479        pos += 4;
480        if version != 2 {
481            return Err(io::Error::new(
482                io::ErrorKind::InvalidData,
483                format!("unsupported slice cache version: {} (expected 2)", version),
484            ));
485        }
486
487        // Number of files
488        let num_files = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
489        pos += 4;
490
491        let mut caches = self.caches.write();
492        let mut current_bytes = self.current_bytes.write();
493        let counter = *self.access_counter.read();
494
495        for _ in 0..num_files {
496            // Path length
497            if pos + 4 > data.len() {
498                return Err(io::Error::new(
499                    io::ErrorKind::InvalidData,
500                    "truncated path length",
501                ));
502            }
503            let path_len = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
504            pos += 4;
505
506            // Path
507            if pos + path_len > data.len() {
508                return Err(io::Error::new(io::ErrorKind::InvalidData, "truncated path"));
509            }
510            let path_str = std::str::from_utf8(&data[pos..pos + path_len])
511                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
512            let path = PathBuf::from(path_str);
513            pos += path_len;
514
515            // File cache
516            let (file_cache, consumed) = FileSliceCache::deserialize(&data[pos..], counter)?;
517            pos += consumed;
518
519            // Merge into existing cache
520            *current_bytes += file_cache.total_bytes;
521            caches.insert(path, file_cache);
522        }
523
524        // Load file sizes
525        if pos + 4 <= data.len() {
526            let num_sizes = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
527            pos += 4;
528
529            let mut file_sizes = self.file_sizes.write();
530            for _ in 0..num_sizes {
531                if pos + 4 > data.len() {
532                    break;
533                }
534                let path_len = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
535                pos += 4;
536
537                if pos + path_len > data.len() {
538                    break;
539                }
540                let path_str = match std::str::from_utf8(&data[pos..pos + path_len]) {
541                    Ok(s) => s,
542                    Err(_) => break,
543                };
544                let path = PathBuf::from(path_str);
545                pos += path_len;
546
547                if pos + 8 > data.len() {
548                    break;
549                }
550                let size = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
551                pos += 8;
552
553                file_sizes.insert(path, size);
554            }
555        }
556
557        Ok(())
558    }
559
560    /// Serialize the cache to a writer
561    pub fn serialize_to_writer<W: Write>(&self, mut writer: W) -> io::Result<()> {
562        let data = self.serialize();
563        writer.write_all(&data)
564    }
565
566    /// Deserialize the cache from a reader
567    pub fn deserialize_from_reader<R: Read>(&self, mut reader: R) -> io::Result<()> {
568        let mut data = Vec::new();
569        reader.read_to_end(&mut data)?;
570        self.deserialize(&data)
571    }
572
573    /// Check if the cache is empty
574    pub fn is_empty(&self) -> bool {
575        *self.current_bytes.read() == 0
576    }
577
578    /// Clear all cached data
579    pub fn clear(&self) {
580        let mut caches = self.caches.write();
581        let mut current_bytes = self.current_bytes.write();
582        caches.clear();
583        *current_bytes = 0;
584    }
585}
586
587/// Cache statistics
588#[derive(Debug, Clone)]
589pub struct SliceCacheStats {
590    pub total_bytes: usize,
591    pub max_bytes: usize,
592    pub total_slices: usize,
593    pub files_cached: usize,
594}
595
596#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
597#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
598impl<D: Directory> Directory for SliceCachingDirectory<D> {
599    async fn exists(&self, path: &Path) -> io::Result<bool> {
600        self.inner.exists(path).await
601    }
602
603    async fn file_size(&self, path: &Path) -> io::Result<u64> {
604        // Check cache first
605        {
606            let file_sizes = self.file_sizes.read();
607            if let Some(&size) = file_sizes.get(path) {
608                return Ok(size);
609            }
610        }
611
612        // Fetch from inner and cache
613        let size = self.inner.file_size(path).await?;
614        {
615            let mut file_sizes = self.file_sizes.write();
616            file_sizes.insert(path.to_path_buf(), size);
617        }
618        Ok(size)
619    }
620
621    async fn open_read(&self, path: &Path) -> io::Result<FileHandle> {
622        // Check if we have the full file cached (use our caching file_size)
623        let file_size = self.file_size(path).await?;
624        let full_range = 0..file_size;
625
626        // Try cache first for full file
627        if let Some(data) = self.try_cache_read(path, full_range.clone()) {
628            return Ok(FileHandle::from_bytes(OwnedBytes::new(data)));
629        }
630
631        // Read from inner
632        let handle = self.inner.open_read(path).await?;
633        let bytes = handle.read_bytes().await?;
634
635        // Cache the full file
636        self.cache_insert(path, full_range, bytes.as_slice().to_vec());
637
638        Ok(FileHandle::from_bytes(bytes))
639    }
640
641    async fn read_range(&self, path: &Path, range: Range<u64>) -> io::Result<OwnedBytes> {
642        // Try cache first
643        if let Some(data) = self.try_cache_read(path, range.clone()) {
644            return Ok(OwnedBytes::new(data));
645        }
646
647        // Read from inner
648        let data = self.inner.read_range(path, range.clone()).await?;
649
650        // Cache the result
651        self.cache_insert(path, range, data.as_slice().to_vec());
652
653        Ok(data)
654    }
655
656    async fn list_files(&self, prefix: &Path) -> io::Result<Vec<PathBuf>> {
657        self.inner.list_files(prefix).await
658    }
659
660    async fn open_lazy(&self, path: &Path) -> io::Result<FileHandle> {
661        // Get file size (uses cache to avoid HEAD requests)
662        let file_size = self.file_size(path).await?;
663
664        // Create a caching wrapper around the inner directory's read_range
665        let path_buf = path.to_path_buf();
666        let caches = Arc::clone(&self.caches);
667        let current_bytes = Arc::clone(&self.current_bytes);
668        let access_counter = Arc::clone(&self.access_counter);
669        let max_bytes = self.max_bytes;
670        let inner = Arc::clone(&self.inner);
671
672        let read_fn: RangeReadFn = Arc::new(move |range: Range<u64>| {
673            let path = path_buf.clone();
674            let caches = Arc::clone(&caches);
675            let current_bytes = Arc::clone(&current_bytes);
676            let access_counter = Arc::clone(&access_counter);
677            let inner = Arc::clone(&inner);
678
679            Box::pin(async move {
680                // Try cache first
681                {
682                    let mut caches_guard = caches.write();
683                    let mut counter = access_counter.write();
684                    if let Some(file_cache) = caches_guard.get_mut(&path)
685                        && let Some(data) = file_cache.try_read(range.clone(), &mut counter)
686                    {
687                        return Ok(OwnedBytes::new(data));
688                    }
689                }
690
691                log::trace!("Cache MISS: {:?} [{}-{}]", path, range.start, range.end);
692
693                // Read from inner
694                let data = inner.read_range(&path, range.clone()).await?;
695
696                // Cache the result
697                let data_len = data.as_slice().len();
698                {
699                    // Check if we need to evict
700                    let current = *current_bytes.read();
701                    if current + data_len > max_bytes {
702                        // Simple eviction: just skip caching if over limit
703                        // Full eviction would require more complex logic here
704                    } else {
705                        let mut caches_guard = caches.write();
706                        let counter = *access_counter.read();
707
708                        let file_cache = caches_guard
709                            .entry(path.clone())
710                            .or_insert_with(FileSliceCache::new);
711
712                        let net_change =
713                            file_cache.insert(range, data.as_slice().to_vec(), counter);
714                        let mut current = current_bytes.write();
715                        if net_change >= 0 {
716                            *current += net_change as usize;
717                        } else {
718                            *current = current.saturating_sub((-net_change) as usize);
719                        }
720                    }
721                }
722
723                Ok(data)
724            })
725        });
726
727        Ok(FileHandle::lazy_labeled(
728            file_size,
729            read_fn,
730            self.label.get(),
731        ))
732    }
733
734    fn set_index_label(&self, label: &str) {
735        self.label.set(label);
736        self.inner.set_index_label(label);
737    }
738}
739
740/// DirectoryWriter implementation for SliceCachingDirectory
741/// Delegates to inner directory and invalidates cache entries as needed
742#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
743#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
744impl<D: super::DirectoryWriter> super::DirectoryWriter for SliceCachingDirectory<D> {
745    async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()> {
746        // Invalidate cache for this file
747        {
748            let mut caches = self.caches.write();
749            if let Some(file_cache) = caches.remove(path) {
750                let mut current = self.current_bytes.write();
751                *current = current.saturating_sub(file_cache.total_bytes);
752            }
753        }
754        // Invalidate file size cache
755        {
756            let mut file_sizes = self.file_sizes.write();
757            file_sizes.remove(path);
758        }
759        // Delegate to inner
760        self.inner.write(path, data).await
761    }
762
763    async fn delete(&self, path: &Path) -> io::Result<()> {
764        // Invalidate cache for this file
765        {
766            let mut caches = self.caches.write();
767            if let Some(file_cache) = caches.remove(path) {
768                let mut current = self.current_bytes.write();
769                *current = current.saturating_sub(file_cache.total_bytes);
770            }
771        }
772        // Invalidate file size cache
773        {
774            let mut file_sizes = self.file_sizes.write();
775            file_sizes.remove(path);
776        }
777        // Delegate to inner
778        self.inner.delete(path).await
779    }
780
781    async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
782        // Move cache entries from old path to new path
783        {
784            let mut caches = self.caches.write();
785            if let Some(file_cache) = caches.remove(from) {
786                caches.insert(to.to_path_buf(), file_cache);
787            }
788        }
789        // Move file size cache
790        {
791            let mut file_sizes = self.file_sizes.write();
792            if let Some(size) = file_sizes.remove(from) {
793                file_sizes.insert(to.to_path_buf(), size);
794            }
795        }
796        // Delegate to inner
797        self.inner.rename(from, to).await
798    }
799
800    async fn sync(&self) -> io::Result<()> {
801        self.inner.sync().await
802    }
803
804    async fn streaming_writer(&self, path: &Path) -> io::Result<Box<dyn super::StreamingWriter>> {
805        // Invalidate cache for this file before writing
806        {
807            let mut caches = self.caches.write();
808            if let Some(file_cache) = caches.remove(path) {
809                let mut current = self.current_bytes.write();
810                *current = current.saturating_sub(file_cache.total_bytes);
811            }
812        }
813        {
814            let mut file_sizes = self.file_sizes.write();
815            file_sizes.remove(path);
816        }
817        self.inner.streaming_writer(path).await
818    }
819}
820
821#[cfg(test)]
822mod tests {
823    use super::*;
824    use crate::directories::{DirectoryWriter, RamDirectory};
825
826    #[tokio::test]
827    async fn test_slice_cache_basic() {
828        let ram = RamDirectory::new();
829        ram.write(Path::new("test.bin"), &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
830            .await
831            .unwrap();
832
833        let cached = SliceCachingDirectory::new(ram, 1024);
834
835        // First read - cache miss
836        let data = cached
837            .read_range(Path::new("test.bin"), 2..5)
838            .await
839            .unwrap();
840        assert_eq!(data.as_slice(), &[2, 3, 4]);
841
842        // Second read - should be cache hit
843        let data = cached
844            .read_range(Path::new("test.bin"), 2..5)
845            .await
846            .unwrap();
847        assert_eq!(data.as_slice(), &[2, 3, 4]);
848
849        let stats = cached.stats();
850        assert_eq!(stats.total_slices, 1);
851        assert_eq!(stats.total_bytes, 3);
852    }
853
854    #[tokio::test]
855    async fn test_slice_cache_overlap_merge() {
856        let ram = RamDirectory::new();
857        ram.write(Path::new("test.bin"), &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
858            .await
859            .unwrap();
860
861        let cached = SliceCachingDirectory::new(ram, 1024);
862
863        // Read [2..5]
864        cached
865            .read_range(Path::new("test.bin"), 2..5)
866            .await
867            .unwrap();
868
869        // Read [4..7] - overlaps with previous
870        cached
871            .read_range(Path::new("test.bin"), 4..7)
872            .await
873            .unwrap();
874
875        let stats = cached.stats();
876        // Should be merged into one slice [2..7]
877        assert_eq!(stats.total_slices, 1);
878        assert_eq!(stats.total_bytes, 5); // bytes 2,3,4,5,6
879
880        // Reading from merged range should work
881        let data = cached
882            .read_range(Path::new("test.bin"), 3..6)
883            .await
884            .unwrap();
885        assert_eq!(data.as_slice(), &[3, 4, 5]);
886    }
887
888    #[tokio::test]
889    async fn test_slice_cache_eviction() {
890        let ram = RamDirectory::new();
891        ram.write(Path::new("test.bin"), &[0; 100]).await.unwrap();
892
893        // Small cache limit
894        let cached = SliceCachingDirectory::new(ram, 50);
895
896        // Fill cache
897        cached
898            .read_range(Path::new("test.bin"), 0..30)
899            .await
900            .unwrap();
901
902        // This should trigger eviction
903        cached
904            .read_range(Path::new("test.bin"), 50..80)
905            .await
906            .unwrap();
907
908        let stats = cached.stats();
909        assert!(stats.total_bytes <= 50);
910    }
911
912    #[tokio::test]
913    async fn test_slice_cache_serialize_deserialize() {
914        let ram = RamDirectory::new();
915        ram.write(Path::new("file1.bin"), &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
916            .await
917            .unwrap();
918        ram.write(Path::new("file2.bin"), &[10, 11, 12, 13, 14, 15])
919            .await
920            .unwrap();
921
922        let cached = SliceCachingDirectory::new(ram.clone(), 1024);
923
924        // Read some ranges to populate cache
925        cached
926            .read_range(Path::new("file1.bin"), 2..6)
927            .await
928            .unwrap();
929        cached
930            .read_range(Path::new("file2.bin"), 1..4)
931            .await
932            .unwrap();
933
934        let stats = cached.stats();
935        assert_eq!(stats.files_cached, 2);
936        assert_eq!(stats.total_bytes, 7); // 4 + 3
937
938        // Serialize
939        let serialized = cached.serialize();
940        assert!(!serialized.is_empty());
941
942        // Create new cache and deserialize
943        let cached2 = SliceCachingDirectory::new(ram.clone(), 1024);
944        assert!(cached2.is_empty());
945
946        cached2.deserialize(&serialized).unwrap();
947
948        let stats2 = cached2.stats();
949        assert_eq!(stats2.files_cached, 2);
950        assert_eq!(stats2.total_bytes, 7);
951
952        // Verify cached data is correct by reading (should be cache hits)
953        let data = cached2
954            .read_range(Path::new("file1.bin"), 2..6)
955            .await
956            .unwrap();
957        assert_eq!(data.as_slice(), &[2, 3, 4, 5]);
958
959        let data = cached2
960            .read_range(Path::new("file2.bin"), 1..4)
961            .await
962            .unwrap();
963        assert_eq!(data.as_slice(), &[11, 12, 13]);
964    }
965
966    #[tokio::test]
967    async fn test_slice_cache_serialize_empty() {
968        let ram = RamDirectory::new();
969        let cached = SliceCachingDirectory::new(ram, 1024);
970
971        // Serialize empty cache
972        let serialized = cached.serialize();
973        assert!(!serialized.is_empty()); // Should have header
974
975        // Deserialize into new cache
976        let cached2 = SliceCachingDirectory::new(RamDirectory::new(), 1024);
977        cached2.deserialize(&serialized).unwrap();
978        assert!(cached2.is_empty());
979    }
980}