Skip to main content

range_cache/
cache.rs

1//! Core cache types.
2
3use std::{
4    collections::BTreeMap,
5    num::NonZeroUsize,
6    ops::{Bound, Range},
7    sync::Arc,
8};
9
10use bytes::Bytes;
11use parking_lot::Mutex;
12
13use crate::RangeError;
14
15/// The maximum number of payload bytes resident in a cache.
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum CacheCapacity {
18    /// Evict least-recently-used ranges to enforce the given byte ceiling.
19    Bounded(NonZeroUsize),
20    /// Retain all admitted ranges until explicit invalidation.
21    Unbounded,
22}
23
24/// The result of an insertion attempt.
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum InsertOutcome {
27    /// The bytes were admitted to the cache.
28    Inserted,
29    /// Existing coverage already fully contained the inserted range.
30    AlreadyCovered,
31    /// The resulting merged range exceeded the bounded capacity.
32    TooLarge,
33}
34
35/// The effect of invalidating cache state.
36#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
37pub struct Invalidation {
38    /// Number of ranges removed.
39    pub ranges: usize,
40    /// Number of payload bytes removed.
41    pub bytes: usize,
42}
43
44/// A point-in-time cache state and statistics snapshot.
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46pub struct CacheSnapshot {
47    /// Configured capacity.
48    pub capacity: CacheCapacity,
49    /// Total resident payload bytes.
50    pub resident_bytes: usize,
51    /// Number of keys with resident ranges.
52    pub keys: usize,
53    /// Number of resident ranges.
54    pub ranges: usize,
55    /// Fully cached reads.
56    pub hits: u64,
57    /// Reads with some, but not all, requested bytes cached.
58    pub partial_hits: u64,
59    /// Reads with none of the requested bytes cached.
60    pub misses: u64,
61    /// Successfully admitted insertions.
62    pub insertions: u64,
63    /// Insertions rejected because the merged range was too large.
64    pub admissions_rejected_too_large: u64,
65    /// Ranges removed by capacity enforcement.
66    pub evictions: u64,
67}
68
69struct CacheBlock {
70    end: usize,
71    bytes: Bytes,
72    last_access: u64,
73}
74
75#[derive(Default)]
76struct Statistics {
77    hits: u64,
78    partial_hits: u64,
79    misses: u64,
80    insertions: u64,
81    admissions_rejected_too_large: u64,
82    evictions: u64,
83}
84
85enum EvictionPolicy<K> {
86    Unbounded,
87    Bounded {
88        // Entries are boxed once, then moved between access keys without
89        // moving a potentially large K through B-tree nodes on every touch.
90        lru: BTreeMap<u64, Box<LruEntry<K>>>,
91        next_access: u64,
92    },
93}
94
95struct LruEntry<K> {
96    key: K,
97    start: usize,
98}
99
100impl<K: Clone> EvictionPolicy<K> {
101    fn register(&mut self, key: &K, start: usize) -> u64 {
102        let Self::Bounded { lru, next_access } = self else {
103            return 0;
104        };
105        let access = *next_access;
106        *next_access = next_access
107            .checked_add(1)
108            .expect("range cache LRU clock exhausted");
109        assert!(
110            lru.insert(
111                access,
112                Box::new(LruEntry {
113                    key: key.clone(),
114                    start,
115                }),
116            )
117            .is_none(),
118            "new range has a unique LRU entry"
119        );
120        access
121    }
122}
123
124impl<K: Ord> EvictionPolicy<K> {
125    #[inline]
126    fn touch(&mut self, key: &K, start: usize, block: &mut CacheBlock) {
127        let Self::Bounded { lru, next_access } = self else {
128            return;
129        };
130        let previous = block.last_access;
131        let Some(resident) = lru.remove(&previous) else {
132            panic!("resident range must have an LRU entry");
133        };
134        debug_assert!(
135            &resident.key == key && resident.start == start,
136            "LRU entry must identify the resident range"
137        );
138
139        let access = *next_access;
140        *next_access = next_access
141            .checked_add(1)
142            .expect("range cache LRU clock exhausted");
143        block.last_access = access;
144        let replaced = lru.insert(access, resident);
145        debug_assert!(replaced.is_none(), "LRU access value is unique");
146    }
147}
148
149struct State<K> {
150    ranges: BTreeMap<K, BTreeMap<usize, CacheBlock>>,
151    eviction: EvictionPolicy<K>,
152    resident_bytes: usize,
153    resident_ranges: usize,
154    statistics: Statistics,
155}
156
157impl<K> State<K> {
158    fn new(capacity: CacheCapacity) -> Self {
159        Self {
160            ranges: BTreeMap::new(),
161            eviction: match capacity {
162                CacheCapacity::Bounded(_) => EvictionPolicy::Bounded {
163                    lru: BTreeMap::new(),
164                    next_access: 0,
165                },
166                CacheCapacity::Unbounded => EvictionPolicy::Unbounded,
167            },
168            resident_bytes: 0,
169            resident_ranges: 0,
170            statistics: Statistics::default(),
171        }
172    }
173}
174
175impl<K: Ord + Clone> State<K> {
176    #[inline]
177    fn touch(&mut self, key: &K, start: usize) {
178        if matches!(self.eviction, EvictionPolicy::Unbounded) {
179            return;
180        }
181        let Self {
182            ranges, eviction, ..
183        } = self;
184        let block = ranges
185            .get_mut(key)
186            .and_then(|ranges| ranges.get_mut(&start))
187            .expect("touched range remains resident");
188        eviction.touch(key, start, block);
189    }
190
191    fn take_block(&mut self, key: &K, start: usize) -> Option<CacheBlock> {
192        let (block, key_is_empty) = {
193            let ranges = self.ranges.get_mut(key)?;
194            let block = ranges.remove(&start)?;
195            (block, ranges.is_empty())
196        };
197
198        if key_is_empty {
199            self.ranges.remove(key);
200        }
201        self.resident_bytes = self
202            .resident_bytes
203            .checked_sub(block.bytes.len())
204            .expect("resident byte accounting cannot underflow");
205        self.resident_ranges = self
206            .resident_ranges
207            .checked_sub(1)
208            .expect("resident range accounting cannot underflow");
209        Some(block)
210    }
211
212    fn remove(&mut self, key: &K, start: usize) -> Option<CacheBlock> {
213        let block = self.take_block(key, start)?;
214        if let EvictionPolicy::Bounded { lru, .. } = &mut self.eviction {
215            let Some(resident) = lru.remove(&block.last_access) else {
216                panic!("removed range must have an LRU entry");
217            };
218            assert!(
219                &resident.key == key && resident.start == start,
220                "LRU entry must identify the removed range"
221            );
222        }
223        Some(block)
224    }
225
226    fn evict_oldest(&mut self) -> CacheBlock {
227        let EvictionPolicy::Bounded { lru, .. } = &mut self.eviction else {
228            panic!("only bounded caches evict");
229        };
230        let Some((access, resident)) = lru.pop_first() else {
231            panic!("resident bytes require an LRU entry");
232        };
233        let LruEntry { key, start } = *resident;
234        let block = self
235            .take_block(&key, start)
236            .expect("LRU range remains resident");
237        assert_eq!(
238            block.last_access, access,
239            "evicted range access value matches its LRU entry"
240        );
241        block
242    }
243}
244
245/// A cloneable, thread-safe sparse cache of byte ranges keyed by `K`.
246pub struct RangeCache<K> {
247    inner: Arc<Mutex<State<K>>>,
248    capacity: CacheCapacity,
249}
250
251impl<K> Clone for RangeCache<K> {
252    fn clone(&self) -> Self {
253        Self {
254            inner: Arc::clone(&self.inner),
255            capacity: self.capacity,
256        }
257    }
258}
259
260impl<K> RangeCache<K> {
261    /// Creates an empty cache with an explicit capacity policy.
262    #[must_use]
263    pub fn new(capacity: CacheCapacity) -> Self {
264        Self {
265            inner: Arc::new(Mutex::new(State::new(capacity))),
266            capacity,
267        }
268    }
269
270    /// Returns the configured capacity policy.
271    #[must_use]
272    pub const fn capacity(&self) -> CacheCapacity {
273        self.capacity
274    }
275}
276
277impl<K: Ord + Clone> RangeCache<K> {
278    /// Returns the requested bytes when the entire range is cached.
279    ///
280    /// A hit contained in one resident range returns a zero-copy [`Bytes`]
281    /// slice. Empty ranges always succeed.
282    ///
283    /// # Errors
284    ///
285    /// Returns [`RangeError::ReversedRange`] when `range.end < range.start`.
286    pub fn get(&self, key: &K, range: Range<usize>) -> Result<Option<Bytes>, RangeError> {
287        validate_range(&range)?;
288        let mut state = self.inner.lock();
289
290        if range.is_empty() {
291            state.statistics.hits += 1;
292            return Ok(Some(Bytes::new()));
293        }
294
295        let hit = state.ranges.get(key).and_then(|ranges| {
296            ranges
297                .range(..=range.start)
298                .next_back()
299                .filter(|(_, block)| range.end <= block.end)
300                .map(|(&start, block)| {
301                    let offset = range.start - start;
302                    (start, block.bytes.slice(offset..offset + range.len()))
303                })
304        });
305        if let Some((start, bytes)) = hit {
306            state.statistics.hits += 1;
307            state.touch(key, start);
308            return Ok(Some(bytes));
309        }
310
311        let mut has_coverage = false;
312        {
313            let State {
314                ranges, eviction, ..
315            } = &mut *state;
316            if let Some(ranges) = ranges.get_mut(key) {
317                if let Some((&start, block)) = ranges
318                    .range_mut(..=range.start)
319                    .next_back()
320                    .filter(|(_, block)| block.end > range.start)
321                {
322                    has_coverage = true;
323                    eviction.touch(key, start, block);
324                }
325                for (&start, block) in
326                    ranges.range_mut((Bound::Excluded(range.start), Bound::Excluded(range.end)))
327                {
328                    has_coverage = true;
329                    eviction.touch(key, start, block);
330                }
331            }
332        }
333        if has_coverage {
334            state.statistics.partial_hits += 1;
335        } else {
336            state.statistics.misses += 1;
337        }
338        Ok(None)
339    }
340
341    /// Returns the gaps within `range` that are not resident for `key`.
342    ///
343    /// # Errors
344    ///
345    /// Returns [`RangeError::ReversedRange`] when `range.end < range.start`.
346    pub fn missing_ranges(
347        &self,
348        key: &K,
349        range: Range<usize>,
350    ) -> Result<Vec<Range<usize>>, RangeError> {
351        validate_range(&range)?;
352        if range.is_empty() {
353            return Ok(Vec::new());
354        }
355
356        let state = self.inner.lock();
357        let Some(ranges) = state.ranges.get(key) else {
358            return Ok(vec![range]);
359        };
360
361        let mut missing = Vec::new();
362        let mut cursor = range.start;
363        if let Some((_, block)) = ranges
364            .range(..=range.start)
365            .next_back()
366            .filter(|(_, block)| block.end > range.start)
367        {
368            cursor = cursor.max(block.end.min(range.end));
369        }
370
371        for (&start, block) in
372            ranges.range((Bound::Excluded(range.start), Bound::Excluded(range.end)))
373        {
374            if cursor < start {
375                missing.push(cursor..start);
376            }
377            cursor = cursor.max(block.end.min(range.end));
378            if cursor == range.end {
379                break;
380            }
381        }
382        if cursor < range.end {
383            missing.push(cursor..range.end);
384        }
385        Ok(missing)
386    }
387
388    /// Inserts `bytes` for `range`, merging adjacent and overlapping ranges.
389    ///
390    /// An insert wholly contained by one existing range is ignored. Otherwise,
391    /// inserted bytes replace overlapping bytes while cached prefix and suffix
392    /// bytes remain intact.
393    ///
394    /// # Errors
395    ///
396    /// Returns [`RangeError::ReversedRange`] for a reversed range or
397    /// [`RangeError::PayloadLengthMismatch`] when the payload length differs
398    /// from the range length.
399    ///
400    /// # Panics
401    ///
402    /// Panics only if an internal range-map, LRU, or byte-accounting invariant
403    /// has been violated.
404    pub fn insert(
405        &self,
406        key: K,
407        range: Range<usize>,
408        bytes: Bytes,
409    ) -> Result<InsertOutcome, RangeError> {
410        validate_range(&range)?;
411        let expected = range.len();
412        if bytes.len() != expected {
413            return Err(RangeError::PayloadLengthMismatch {
414                range,
415                expected,
416                actual: bytes.len(),
417            });
418        }
419        if range.is_empty() {
420            return Ok(InsertOutcome::AlreadyCovered);
421        }
422
423        let mut state = self.inner.lock();
424        let containing = state.ranges.get(&key).and_then(|ranges| {
425            ranges
426                .range(..=range.start)
427                .next_back()
428                .filter(|(_, block)| range.end <= block.end)
429        });
430        if containing.is_some() {
431            return Ok(InsertOutcome::AlreadyCovered);
432        }
433
434        let mut merged_start = range.start;
435        let mut merged_end = range.end;
436        let mut affected = Vec::new();
437        if let Some(ranges) = state.ranges.get(&key) {
438            let following = match ranges.range(..=range.start).next_back() {
439                Some((&start, block)) if block.end >= range.start => {
440                    merged_start = start;
441                    merged_end = merged_end.max(block.end);
442                    affected.push((start, block.end));
443                    Bound::Excluded(start)
444                }
445                Some(_) | None => Bound::Included(range.start),
446            };
447
448            for (&start, block) in ranges.range((following, Bound::Unbounded)) {
449                if start > merged_end {
450                    break;
451                }
452                merged_end = merged_end.max(block.end);
453                affected.push((start, block.end));
454            }
455        }
456
457        let merged_length = merged_end - merged_start;
458        match self.capacity {
459            CacheCapacity::Bounded(capacity) if merged_length > capacity.get() => {
460                state.statistics.admissions_rejected_too_large += 1;
461                return Ok(InsertOutcome::TooLarge);
462            }
463            CacheCapacity::Bounded(_) | CacheCapacity::Unbounded => {}
464        }
465
466        let merged_bytes =
467            if affected.is_empty() || (merged_start == range.start && merged_end == range.end) {
468                bytes
469            } else {
470                let mut merged = vec![0; merged_length];
471                for &(start, end) in &affected {
472                    let cached = &state.ranges[&key][&start].bytes;
473                    let offset = start - merged_start;
474                    merged[offset..offset + (end - start)].copy_from_slice(cached);
475                }
476                let offset = range.start - merged_start;
477                merged[offset..offset + expected].copy_from_slice(&bytes);
478                Bytes::from(merged)
479            };
480
481        for (start, _) in affected {
482            state
483                .remove(&key, start)
484                .expect("affected range remains resident");
485        }
486
487        let access = state.eviction.register(&key, merged_start);
488        let previous = state.ranges.entry(key).or_default().insert(
489            merged_start,
490            CacheBlock {
491                end: merged_end,
492                bytes: merged_bytes,
493                last_access: access,
494            },
495        );
496        assert!(previous.is_none(), "merged range start must be vacant");
497        state.resident_bytes += merged_length;
498        state.resident_ranges += 1;
499        state.statistics.insertions += 1;
500
501        if let CacheCapacity::Bounded(capacity) = self.capacity {
502            while state.resident_bytes > capacity.get() {
503                let _ = state.evict_oldest();
504                state.statistics.evictions += 1;
505            }
506        }
507
508        Ok(InsertOutcome::Inserted)
509    }
510
511    /// Removes all ranges associated with `key`.
512    ///
513    /// # Panics
514    ///
515    /// Panics only if an internal range-map, LRU, or byte-accounting invariant
516    /// has been violated.
517    #[must_use]
518    pub fn invalidate(&self, key: &K) -> Invalidation {
519        let mut state = self.inner.lock();
520        let starts = state
521            .ranges
522            .get(key)
523            .map(|ranges| ranges.keys().copied().collect::<Vec<_>>())
524            .unwrap_or_default();
525        let mut invalidation = Invalidation::default();
526        for start in starts {
527            let block = state
528                .remove(key, start)
529                .expect("invalidated range remains resident");
530            invalidation.ranges += 1;
531            invalidation.bytes += block.bytes.len();
532        }
533        invalidation
534    }
535
536    /// Removes every resident range while retaining accumulated statistics.
537    #[must_use]
538    pub fn clear(&self) -> Invalidation {
539        let mut state = self.inner.lock();
540        let invalidation = Invalidation {
541            ranges: state.resident_ranges,
542            bytes: state.resident_bytes,
543        };
544        state.ranges.clear();
545        if let EvictionPolicy::Bounded { lru, .. } = &mut state.eviction {
546            lru.clear();
547        }
548        state.resident_bytes = 0;
549        state.resident_ranges = 0;
550        invalidation
551    }
552
553    /// Returns a consistent snapshot of cache state and lifetime statistics.
554    #[must_use]
555    pub fn snapshot(&self) -> CacheSnapshot {
556        let state = self.inner.lock();
557        CacheSnapshot {
558            capacity: self.capacity,
559            resident_bytes: state.resident_bytes,
560            keys: state.ranges.len(),
561            ranges: state.resident_ranges,
562            hits: state.statistics.hits,
563            partial_hits: state.statistics.partial_hits,
564            misses: state.statistics.misses,
565            insertions: state.statistics.insertions,
566            admissions_rejected_too_large: state.statistics.admissions_rejected_too_large,
567            evictions: state.statistics.evictions,
568        }
569    }
570
571    #[cfg(feature = "async")]
572    pub(crate) fn read_plan(&self, key: &K, range: Range<usize>) -> Result<ReadPlan, RangeError> {
573        validate_range(&range)?;
574        let mut state = self.inner.lock();
575        if range.is_empty() {
576            state.statistics.hits += 1;
577            return Ok(ReadPlan::Complete(Bytes::new()));
578        }
579
580        let hit = state.ranges.get(key).and_then(|ranges| {
581            ranges
582                .range(..=range.start)
583                .next_back()
584                .filter(|(_, block)| range.end <= block.end)
585                .map(|(&start, block)| {
586                    let offset = range.start - start;
587                    (start, block.bytes.slice(offset..offset + range.len()))
588                })
589        });
590        if let Some((start, bytes)) = hit {
591            state.statistics.hits += 1;
592            state.touch(key, start);
593            return Ok(ReadPlan::Complete(bytes));
594        }
595
596        let mut cached = Vec::new();
597        let mut missing = Vec::new();
598        let mut has_coverage = false;
599        let mut cursor = range.start;
600        {
601            let State {
602                ranges, eviction, ..
603            } = &mut *state;
604            if let Some(ranges) = ranges.get_mut(key) {
605                if let Some((&start, block)) = ranges
606                    .range_mut(..=range.start)
607                    .next_back()
608                    .filter(|(_, block)| block.end > range.start)
609                {
610                    let covered_end = block.end.min(range.end);
611                    let offset = range.start - start;
612                    cached.push((
613                        range.start..covered_end,
614                        block
615                            .bytes
616                            .slice(offset..offset + covered_end - range.start),
617                    ));
618                    has_coverage = true;
619                    cursor = cursor.max(covered_end);
620                    eviction.touch(key, start, block);
621                }
622
623                for (&start, block) in
624                    ranges.range_mut((Bound::Excluded(range.start), Bound::Excluded(range.end)))
625                {
626                    if cursor < start {
627                        missing.push(cursor..start);
628                    }
629                    let covered_end = block.end.min(range.end);
630                    cached.push((start..covered_end, block.bytes.slice(..covered_end - start)));
631                    has_coverage = true;
632                    cursor = cursor.max(covered_end);
633                    eviction.touch(key, start, block);
634                    if cursor == range.end {
635                        break;
636                    }
637                }
638            }
639        }
640        if cursor < range.end {
641            missing.push(cursor..range.end);
642        }
643
644        if has_coverage {
645            state.statistics.partial_hits += 1;
646        } else {
647            state.statistics.misses += 1;
648        }
649        Ok(ReadPlan::Fetch { cached, missing })
650    }
651}
652
653fn validate_range(range: &Range<usize>) -> Result<(), RangeError> {
654    if range.start > range.end {
655        return Err(RangeError::ReversedRange {
656            start: range.start,
657            end: range.end,
658        });
659    }
660    Ok(())
661}
662
663#[cfg(feature = "async")]
664pub(crate) enum ReadPlan {
665    Complete(Bytes),
666    Fetch {
667        cached: Vec<(Range<usize>, Bytes)>,
668        missing: Vec<Range<usize>>,
669    },
670}
671
672#[cfg(test)]
673mod tests {
674    use std::{collections::BTreeMap, num::NonZeroUsize, sync::Arc};
675
676    use bytes::Bytes;
677    use parking_lot::Mutex;
678
679    use super::{CacheBlock, CacheCapacity, EvictionPolicy, RangeCache, State};
680
681    fn bounded_state() -> State<&'static str> {
682        State::new(CacheCapacity::Bounded(
683            NonZeroUsize::new(8).expect("test capacity is non-zero"),
684        ))
685    }
686
687    fn add_untracked_block(state: &mut State<&'static str>) {
688        state.ranges.entry("key").or_default().insert(
689            0,
690            CacheBlock {
691                end: 1,
692                bytes: Bytes::from_static(b"x"),
693                last_access: 0,
694            },
695        );
696        state.resident_bytes = 1;
697        state.resident_ranges = 1;
698    }
699
700    #[test]
701    #[should_panic(expected = "resident range must have an LRU entry")]
702    fn touching_an_untracked_bounded_range_panics() {
703        let mut state = bounded_state();
704        add_untracked_block(&mut state);
705        state.touch(&"key", 0);
706    }
707
708    #[test]
709    #[should_panic(expected = "removed range must have an LRU entry")]
710    fn removing_an_untracked_bounded_range_panics() {
711        let mut state = bounded_state();
712        add_untracked_block(&mut state);
713        let _ = state.remove(&"key", 0);
714    }
715
716    #[test]
717    #[should_panic(expected = "only bounded caches evict")]
718    fn unbounded_state_cannot_evict() {
719        let mut state = State::<u8>::new(CacheCapacity::Unbounded);
720        let _ = state.evict_oldest();
721    }
722
723    #[test]
724    #[should_panic(expected = "resident bytes require an LRU entry")]
725    fn empty_bounded_state_cannot_evict() {
726        let mut state = State::<u8>::new(CacheCapacity::Bounded(
727            NonZeroUsize::new(1).expect("test capacity is non-zero"),
728        ));
729        let _ = state.evict_oldest();
730    }
731
732    #[test]
733    #[should_panic(expected = "range cache LRU clock exhausted")]
734    fn registering_after_lru_clock_exhaustion_panics() {
735        let mut eviction = EvictionPolicy::Bounded {
736            lru: BTreeMap::default(),
737            next_access: u64::MAX,
738        };
739        let _ = eviction.register(&"key", 0);
740    }
741
742    #[test]
743    fn absent_internal_ranges_are_noops() {
744        let mut state = State::<&str>::new(CacheCapacity::Unbounded);
745        let _ = state.take_block(&"missing", 0);
746        state.ranges.insert("empty", BTreeMap::default());
747        let _ = state.take_block(&"empty", 0);
748        let _ = state.remove(&"missing", 0);
749        assert_eq!(state.resident_bytes, 0);
750        assert_eq!(state.resident_ranges, 0);
751    }
752
753    #[test]
754    fn overlapping_internal_ranges_do_not_create_negative_gaps() {
755        let mut state = State::new(CacheCapacity::Unbounded);
756        state.ranges.entry("key").or_default().insert(
757            0,
758            CacheBlock {
759                end: 4,
760                bytes: Bytes::from_static(b"abcd"),
761                last_access: 0,
762            },
763        );
764        state.ranges.entry("key").or_default().insert(
765            2,
766            CacheBlock {
767                end: 6,
768                bytes: Bytes::from_static(b"cdef"),
769                last_access: 0,
770            },
771        );
772        state.resident_bytes = 8;
773        state.resident_ranges = 2;
774        let cache = RangeCache {
775            inner: Arc::new(Mutex::new(state)),
776            capacity: CacheCapacity::Unbounded,
777        };
778
779        assert_eq!(
780            cache.missing_ranges(&"key", 0..6).expect("valid range"),
781            Vec::<std::ops::Range<usize>>::new()
782        );
783        #[cfg(feature = "async")]
784        let _ = cache.read_plan(&"key", 0..6).expect("valid read plan");
785    }
786}