Skip to main content

fast_pull/cache/
seq.rs

1//! Pusher cache that reorders out-of-order chunks into sequential order.
2
3use crate::{ProgressEntry, ProgressListener, Pusher};
4use bytes::Bytes;
5use std::collections::BTreeMap;
6
7/// Pusher wrapper that reorders out-of-order chunks into sequential order.
8///
9/// Buffers chunks in a `BTreeMap` keyed by offset. Eviction starts once the buffered
10/// size reaches `high_watermark` and hands chunks to the inner pusher in ascending
11/// offset order — across gaps if need be, so memory stays bounded while a gap is
12/// still unfilled — until the size falls back to `low_watermark`. It then keeps going
13/// for as long as the next chunk continues the one just written, so a contiguous run
14/// is never cut in half. `flush` drains the whole buffer.
15#[derive(Debug)]
16pub struct CacheSeqPusher<P> {
17    inner: P,
18    cache: BTreeMap<u64, Bytes>,
19    cache_size: usize,
20    high_watermark: usize,
21    low_watermark: usize,
22}
23
24impl<P: Pusher> CacheSeqPusher<P> {
25    /// Wrap `inner` with the given `high_watermark` / `low_watermark` (in bytes).
26    ///
27    /// Eviction to the inner pusher triggers once the buffered size reaches
28    /// `high_watermark`, and stops once it falls back to `low_watermark`.
29    ///
30    /// `low_watermark` must not exceed `high_watermark`. A larger `low_watermark`
31    /// makes `high_watermark` irrelevant, because eviction stops as soon as the
32    /// buffered size is at or below `low_watermark`, which then acts as the sole
33    /// watermark.
34    pub const fn new(inner: P, high_watermark: usize, low_watermark: usize) -> Self {
35        Self {
36            inner,
37            cache: BTreeMap::new(),
38            cache_size: 0,
39            high_watermark,
40            low_watermark,
41        }
42    }
43
44    fn evict_until(&mut self, target_size: usize) -> Result<(), P::Error> {
45        let mut expected = None;
46        while let Some(entry) = self.cache.first_entry() {
47            let start = *entry.key();
48            if self.cache_size <= target_size && Some(start) != expected {
49                break;
50            }
51            let chunk = entry.remove();
52            let chunk_len = chunk.len();
53            let next_pos = start + chunk_len as u64;
54            self.cache_size -= chunk_len;
55            if let Err((e, ret)) = self.inner.push(&(start..next_pos), chunk) {
56                if !ret.is_empty() {
57                    let written = chunk_len.saturating_sub(ret.len());
58                    self.cache_size += ret.len();
59                    if let Some(old) = self.cache.insert(start + written as u64, ret) {
60                        self.cache_size -= old.len();
61                    }
62                }
63                return Err(e);
64            }
65            expected = Some(next_pos);
66        }
67        Ok(())
68    }
69}
70
71impl<P: Pusher> Pusher for CacheSeqPusher<P> {
72    type Error = P::Error;
73
74    fn set_listener(&mut self, cb: ProgressListener) {
75        self.inner.set_listener(cb);
76    }
77
78    fn push(&mut self, range: &ProgressEntry, bytes: Bytes) -> Result<(), (Self::Error, Bytes)> {
79        if bytes.is_empty() {
80            return Ok(());
81        }
82
83        self.cache_size += bytes.len();
84        if let Some(old_bytes) = self.cache.insert(range.start, bytes) {
85            self.cache_size -= old_bytes.len();
86        }
87
88        if self.cache_size >= self.high_watermark
89            && let Err(e) = self.evict_until(self.low_watermark)
90        {
91            return Err((e, Bytes::new()));
92        }
93
94        Ok(())
95    }
96
97    fn flush(&mut self) -> Result<(), Self::Error> {
98        self.evict_until(0)?;
99        self.inner.flush()
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    #![allow(clippy::unwrap_used)]
106    use super::*;
107    use std::sync::atomic::{AtomicBool, Ordering};
108    use std::sync::{Arc, Mutex};
109
110    /// Records every push and can be told to fail the next one (with the bytes
111    /// returned so the cache can re-buffer them).
112    #[derive(Clone)]
113    struct RecordingSink {
114        pushes: Arc<Mutex<Vec<(ProgressEntry, Bytes)>>>,
115        fail_next: Arc<AtomicBool>,
116        listener_set: Arc<AtomicBool>,
117    }
118    impl RecordingSink {
119        fn new() -> Self {
120            Self {
121                pushes: Arc::new(Mutex::new(Vec::new())),
122                fail_next: Arc::new(AtomicBool::new(false)),
123                listener_set: Arc::new(AtomicBool::new(false)),
124            }
125        }
126    }
127    impl Pusher for RecordingSink {
128        type Error = std::io::Error;
129        fn set_listener(&mut self, _: ProgressListener) {
130            self.listener_set.store(true, Ordering::SeqCst);
131        }
132        fn push(
133            &mut self,
134            range: &ProgressEntry,
135            bytes: Bytes,
136        ) -> Result<(), (Self::Error, Bytes)> {
137            if self.fail_next.fetch_and(false, Ordering::SeqCst) {
138                return Err((std::io::Error::other("boom"), bytes));
139            }
140            self.pushes.lock().unwrap().push((range.clone(), bytes));
141            Ok(())
142        }
143        fn flush(&mut self) -> Result<(), Self::Error> {
144            Ok(())
145        }
146    }
147
148    /// Inner pusher that writes only the first 2 bytes of the chunk on its first call,
149    /// then fails returning the unwritten tail as `rem`. Exercises `evict_until`'s
150    /// partial-write re-buffering.
151    #[derive(Clone)]
152    struct PartialSink {
153        pushes: Arc<Mutex<Vec<(ProgressEntry, Bytes)>>>,
154        partial: Arc<AtomicBool>,
155    }
156    impl Pusher for PartialSink {
157        type Error = std::io::Error;
158        fn push(
159            &mut self,
160            range: &ProgressEntry,
161            bytes: Bytes,
162        ) -> Result<(), (Self::Error, Bytes)> {
163            self.pushes
164                .lock()
165                .unwrap()
166                .push((range.clone(), bytes.clone()));
167            if !self.partial.swap(true, Ordering::SeqCst) {
168                let rem = bytes.slice(2..);
169                return Err((std::io::Error::other("partial"), rem));
170            }
171            Ok(())
172        }
173    }
174
175    fn bb(s: &str) -> Bytes {
176        Bytes::copy_from_slice(s.as_bytes())
177    }
178
179    #[test]
180    fn empty_push_is_noop() {
181        // Line 69: a zero-length chunk returns `Ok(())` immediately.
182        let sink = RecordingSink::new();
183        let mut p = CacheSeqPusher::new(sink.clone(), 100, 0);
184        p.push(&(0..0), Bytes::new()).unwrap();
185        assert!(sink.pushes.lock().unwrap().is_empty());
186    }
187
188    #[test]
189    fn duplicate_start_overwrites_old_bytes() {
190        // Line 74: re-inserting at an already-cached start replaces the old entry
191        // and adjusts `cache_size` so the total does not grow.
192        let sink = RecordingSink::new();
193        let mut p = CacheSeqPusher::new(sink.clone(), 100, 0);
194        p.push(&(0..10), bb(&"A".repeat(10))).unwrap();
195        p.push(&(0..10), bb(&"B".repeat(10))).unwrap();
196        p.flush().unwrap();
197        let pushes = sink.pushes.lock().unwrap();
198        assert_eq!(pushes.len(), 1);
199        assert_eq!(&pushes[0].1[..], b"BBBBBBBBBB");
200        drop(pushes);
201    }
202
203    #[test]
204    fn evict_stops_at_gap_once_below_target() {
205        // Line 41: when the cache has dropped to <= low_watermark and the next
206        // cached chunk is not contiguous with what was just flushed, eviction stops.
207        let sink = RecordingSink::new();
208        let mut p = CacheSeqPusher::new(sink.clone(), 100, 30);
209        p.push(&(0..40), bb(&"A".repeat(40))).unwrap();
210        p.push(&(40..80), bb(&"B".repeat(40))).unwrap();
211        // This insertion reaches the high watermark (100) and triggers eviction to 30.
212        // During eviction A (40) then B (40) are flushed; cache_size drops to 20 (<= 30)
213        // but the next buffered chunk starts at 200 (a gap, not contiguous with the
214        // previous flush end of 80), so the loop breaks at line 41 and C stays buffered.
215        p.push(&(200..220), bb(&"C".repeat(20))).unwrap();
216
217        let pushes = sink.pushes.lock().unwrap();
218        assert_eq!(pushes.len(), 2);
219        assert_eq!(pushes[0].0, 0..40);
220        assert_eq!(pushes[1].0, 40..80);
221        drop(pushes);
222    }
223
224    #[test]
225    fn inner_push_failure_during_push_eviction_propagates() {
226        // Lines 47-52 (re-buffer remainder) and 78-80 (propagate the error from `push`).
227        let sink = RecordingSink::new();
228        sink.fail_next.store(true, Ordering::SeqCst);
229        let mut p = CacheSeqPusher::new(sink, 10, 0);
230        let res = p.push(&(0..10), bb(&"A".repeat(10)));
231        assert!(res.is_err());
232    }
233
234    #[test]
235    fn inner_push_failure_during_flush_propagates() {
236        // Lines 47-52 via `flush`: the failing chunk is re-buffered and the error surfaces.
237        let sink = RecordingSink::new();
238        sink.fail_next.store(true, Ordering::SeqCst);
239        let mut p = CacheSeqPusher::new(sink, 100, 0);
240        p.push(&(0..10), bb(&"A".repeat(10))).unwrap();
241        assert!(p.flush().is_err());
242    }
243
244    #[test]
245    fn set_listener_forwards_to_inner() {
246        // Lines 63-65: the listener is forwarded to the inner pusher.
247        let sink = RecordingSink::new();
248        let mut p = CacheSeqPusher::new(sink.clone(), 100, 0);
249        p.set_listener(Box::new(|_| {}));
250        assert!(sink.listener_set.load(Ordering::SeqCst));
251    }
252
253    #[test]
254    fn evict_breaks_on_gap_below_target() {
255        // Line 41: once `cache_size` has dropped to <= target but the next
256        // buffered chunk is not the expected sequential position, eviction
257        // stops early and the gapped chunk remains buffered.
258        let sink = RecordingSink::new();
259        let mut p = CacheSeqPusher::new(sink.clone(), 20, 10);
260        p.push(&(0..10), bb(&"A".repeat(10))).unwrap();
261        p.push(&(20..30), bb(&"B".repeat(10))).unwrap();
262        // [0..10] is flushed; [20..30] stays buffered because of the gap at 10.
263        let pushes = sink.pushes.lock().unwrap();
264        assert_eq!(pushes.len(), 1);
265        assert_eq!(pushes[0].0, 0..10);
266        drop(pushes);
267    }
268
269    #[test]
270    fn evict_until_partial_write_rebuffers_tail_at_correct_offset() {
271        // A partial inner write (first 2 of 10 bytes persisted, then failure with the
272        // 8-byte tail as `rem`) must re-buffer the tail at offset 2 and flush only it on retry.
273        let sink = PartialSink {
274            pushes: Arc::new(Mutex::new(Vec::new())),
275            partial: Arc::new(AtomicBool::new(false)),
276        };
277        let mut p = CacheSeqPusher::new(sink.clone(), 10, 0);
278        // Reaches high watermark (10) and evicts: the partial failure re-buffers [2..10).
279        let res = p.push(&(0..10), bb(&"A".repeat(10)));
280        assert!(res.is_err());
281        // Retry flushes the retained tail, not the already-written prefix.
282        p.flush().unwrap();
283        let pushes = sink.pushes.lock().unwrap();
284        assert_eq!(pushes.len(), 2);
285        assert_eq!(pushes[0].0, 0..10); // original chunk handed to inner
286        assert_eq!(pushes[1].0, 2..10); // tail re-buffered at correct offset
287        assert_eq!(&pushes[1].1[..], b"AAAAAAAA");
288    }
289
290    #[test]
291    fn evict_continues_across_gaps_while_above_target() {
292        // The "across gaps if need be" guarantee: while `cache_size` is still above
293        // `low_watermark`, eviction must not stop at a gap — it keeps draining so memory
294        // stays bounded even when an earlier offset is still missing. Here every chunk
295        // has a gap before it, yet all three are flushed because `cache_size` stays
296        // above `low_watermark` until the very last one.
297        let sink = RecordingSink::new();
298        let mut p = CacheSeqPusher::new(sink.clone(), 100, 30);
299        p.push(&(0..40), bb(&"A".repeat(40))).unwrap();
300        p.push(&(200..240), bb(&"B".repeat(40))).unwrap();
301        p.push(&(400..440), bb(&"C".repeat(40))).unwrap();
302        let pushes = sink.pushes.lock().unwrap();
303        assert_eq!(pushes.len(), 3);
304        assert_eq!(pushes[0].0, 0..40);
305        assert_eq!(pushes[1].0, 200..240);
306        assert_eq!(pushes[2].0, 400..440);
307    }
308
309    #[test]
310    fn failed_push_eviction_retains_callers_chunk_for_retry() {
311        // When a push-triggered eviction fails, the caller's chunk is held internally
312        // (empty remainder returned) and must still be written by a later flush.
313        let sink = RecordingSink::new();
314        sink.fail_next.store(true, Ordering::SeqCst);
315        let mut p = CacheSeqPusher::new(sink.clone(), 10, 0);
316        let res = p.push(&(0..10), bb(&"A".repeat(10)));
317        assert!(res.is_err());
318        assert_eq!(
319            res.unwrap_err().1.len(),
320            0,
321            "buffered data is held internally, not returned to the caller"
322        );
323        p.flush().unwrap();
324        let pushes = sink.pushes.lock().unwrap();
325        assert_eq!(pushes.len(), 1);
326        assert_eq!(pushes[0].0, 0..10);
327    }
328}