Skip to main content

fast_pull/cache/
direct.rs

1//! Pusher cache that flushes contiguous runs without byte merging.
2
3use crate::{ProgressEntry, ProgressListener, Pusher};
4use bytes::Bytes;
5use std::collections::BTreeMap;
6
7/// Pusher wrapper that buffers chunks and flushes large contiguous runs without merging.
8///
9/// Out-of-order chunks are stored in a `BTreeMap`. When a contiguous run reaches
10/// the high watermark, it is flushed to the inner pusher as-is (no byte merging).
11/// This reduces CPU overhead compared to [`super::CacheMergePusher`] at the cost of
12/// more individual write calls.
13#[derive(Debug)]
14pub struct CacheDirectPusher<P> {
15    inner: P,
16    cache: BTreeMap<u64, Bytes>,
17    cache_size: usize,
18    high_watermark: usize,
19    low_watermark: usize,
20}
21
22impl<P: Pusher> CacheDirectPusher<P> {
23    /// Wrap `inner` with the given `high_watermark` / `low_watermark` (in bytes).
24    ///
25    /// Eviction to the inner pusher triggers once the buffered size reaches
26    /// `high_watermark`, and stops once it falls back to `low_watermark`.
27    ///
28    /// `low_watermark` must not exceed `high_watermark`. A larger `low_watermark`
29    /// makes `high_watermark` irrelevant, because eviction does nothing until the
30    /// buffered size passes `low_watermark`, which then acts as the sole watermark.
31    ///
32    /// With `high_watermark == low_watermark`, a push that lands the buffered size
33    /// exactly on the watermark evicts nothing; the next push takes it above and
34    /// eviction proceeds as usual.
35    pub const fn new(inner: P, high_watermark: usize, low_watermark: usize) -> Self {
36        Self {
37            inner,
38            cache: BTreeMap::new(),
39            cache_size: 0,
40            high_watermark,
41            low_watermark,
42        }
43    }
44
45    fn evict_until(&mut self, target_size: usize) -> Result<(), P::Error> {
46        if self.cache_size <= target_size {
47            return Ok(());
48        }
49
50        let mut runs: Vec<(u64, usize)> = Vec::with_capacity(self.cache.len());
51        let mut curr_start = None;
52        let mut curr_len = 0;
53        let mut expected_next = 0;
54
55        for (&start, bytes) in &self.cache {
56            let len = bytes.len();
57            if let Some(c_start) = curr_start {
58                if start == expected_next {
59                    curr_len += len;
60                    expected_next += len as u64;
61                } else {
62                    runs.push((c_start, curr_len));
63                    curr_start = Some(start);
64                    curr_len = len;
65                    expected_next = start + len as u64;
66                }
67            } else {
68                curr_start = Some(start);
69                curr_len = len;
70                expected_next = start + len as u64;
71            }
72        }
73        if let Some(c_start) = curr_start {
74            runs.push((c_start, curr_len));
75        }
76        runs.sort_unstable_by_key(|&(_, len)| std::cmp::Reverse(len));
77
78        for (mut start, mut total_len) in runs {
79            while total_len > 0 {
80                let chunk = self.cache.remove(&start).unwrap();
81                let len = chunk.len();
82                self.cache_size -= len;
83                total_len -= len;
84                let range = start..start + len as u64;
85                if let Err((e, ret_bytes)) = self.inner.push(&range, chunk) {
86                    if !ret_bytes.is_empty() {
87                        self.cache_size += ret_bytes.len();
88                        let written = len.saturating_sub(ret_bytes.len());
89                        if let Some(old) = self.cache.insert(start + written as u64, ret_bytes) {
90                            self.cache_size -= old.len();
91                        }
92                    }
93                    return Err(e);
94                }
95                start += len as u64;
96            }
97            if self.cache_size <= target_size {
98                break;
99            }
100        }
101        Ok(())
102    }
103}
104
105impl<P: Pusher> Pusher for CacheDirectPusher<P> {
106    type Error = P::Error;
107
108    fn set_listener(&mut self, cb: ProgressListener) {
109        self.inner.set_listener(cb);
110    }
111
112    fn push(&mut self, range: &ProgressEntry, bytes: Bytes) -> Result<(), (Self::Error, Bytes)> {
113        if bytes.is_empty() {
114            return Ok(());
115        }
116
117        self.cache_size += bytes.len();
118        if let Some(old_bytes) = self.cache.insert(range.start, bytes) {
119            self.cache_size -= old_bytes.len();
120        }
121
122        if self.cache_size >= self.high_watermark
123            && let Err(e) = self.evict_until(self.low_watermark)
124        {
125            return Err((e, Bytes::new()));
126        }
127
128        Ok(())
129    }
130
131    fn flush(&mut self) -> Result<(), Self::Error> {
132        self.evict_until(0)?;
133        self.inner.flush()
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    #![allow(clippy::unwrap_used)]
140    use super::*;
141    use std::sync::atomic::{AtomicBool, Ordering};
142    use std::sync::{Arc, Mutex};
143
144    /// Records every push and can be told to fail the next one, returning the
145    /// bytes so the cache can re-buffer them.
146    #[derive(Clone)]
147    struct RecordingSink {
148        pushes: Arc<Mutex<Vec<(ProgressEntry, Bytes)>>>,
149        fail_next: Arc<AtomicBool>,
150        listener_set: Arc<AtomicBool>,
151    }
152    impl RecordingSink {
153        fn new() -> Self {
154            Self {
155                pushes: Arc::new(Mutex::new(Vec::new())),
156                fail_next: Arc::new(AtomicBool::new(false)),
157                listener_set: Arc::new(AtomicBool::new(false)),
158            }
159        }
160    }
161    impl Pusher for RecordingSink {
162        type Error = std::io::Error;
163        fn set_listener(&mut self, _: ProgressListener) {
164            self.listener_set.store(true, Ordering::SeqCst);
165        }
166        fn push(
167            &mut self,
168            range: &ProgressEntry,
169            bytes: Bytes,
170        ) -> Result<(), (Self::Error, Bytes)> {
171            if self.fail_next.fetch_and(false, Ordering::SeqCst) {
172                return Err((std::io::Error::other("boom"), bytes));
173            }
174            self.pushes.lock().unwrap().push((range.clone(), bytes));
175            Ok(())
176        }
177        fn flush(&mut self) -> Result<(), Self::Error> {
178            Ok(())
179        }
180    }
181
182    /// Inner pusher that writes only the first 2 bytes of the chunk on its first call,
183    /// then fails returning the unwritten tail as `rem`. Exercises `evict_until`'s
184    /// per-chunk partial-write re-buffering.
185    #[derive(Clone)]
186    struct PartialSinkDirect {
187        pushes: Arc<Mutex<Vec<(ProgressEntry, Bytes)>>>,
188        partial: Arc<AtomicBool>,
189    }
190    impl Pusher for PartialSinkDirect {
191        type Error = std::io::Error;
192        fn push(
193            &mut self,
194            range: &ProgressEntry,
195            bytes: Bytes,
196        ) -> Result<(), (Self::Error, Bytes)> {
197            self.pushes
198                .lock()
199                .unwrap()
200                .push((range.clone(), bytes.clone()));
201            if !self.partial.swap(true, Ordering::SeqCst) {
202                let rem = bytes.slice(2..);
203                return Err((std::io::Error::other("partial"), rem));
204            }
205            Ok(())
206        }
207    }
208
209    fn bb(s: &str) -> Bytes {
210        Bytes::copy_from_slice(s.as_bytes())
211    }
212
213    #[test]
214    fn empty_push_is_noop() {
215        // Lines 103-105: a zero-length chunk returns `Ok(())` without buffering.
216        let sink = RecordingSink::new();
217        let mut p = CacheDirectPusher::new(sink.clone(), 100, 0);
218        p.push(&(0..0), Bytes::new()).unwrap();
219        assert!(sink.pushes.lock().unwrap().is_empty());
220    }
221
222    #[test]
223    fn equal_watermarks_skip_only_the_exact_hit() {
224        // With high == low, `evict_until` returns early only while `cache_size` is
225        // exactly on the watermark. It is not a permanent no-op: the next push takes
226        // the buffer above the watermark and eviction runs normally.
227        let sink = RecordingSink::new();
228        let mut p = CacheDirectPusher::new(sink.clone(), 10, 10);
229        p.push(&(0..10), bb(&"A".repeat(10))).unwrap();
230        assert!(sink.pushes.lock().unwrap().is_empty());
231
232        p.push(&(10..11), bb("B")).unwrap();
233        let ranges: Vec<_> = sink
234            .pushes
235            .lock()
236            .unwrap()
237            .iter()
238            .map(|(r, _)| r.clone())
239            .collect();
240        assert_eq!(ranges, vec![0..10, 10..11]);
241    }
242
243    #[test]
244    fn below_watermark_buffers_until_flush() {
245        // No eviction happens while below the high watermark; flush pushes each chunk.
246        let sink = RecordingSink::new();
247        let mut p = CacheDirectPusher::new(sink.clone(), 100, 10);
248        p.push(&(0..10), bb(&"A".repeat(10))).unwrap();
249        p.push(&(10..20), bb(&"B".repeat(10))).unwrap();
250        assert!(sink.pushes.lock().unwrap().is_empty());
251        p.flush().unwrap();
252        let pushes = sink.pushes.lock().unwrap();
253        assert_eq!(pushes.len(), 2);
254        assert_eq!(pushes[0].0, 0..10);
255        assert_eq!(pushes[1].0, 10..20);
256        drop(pushes);
257    }
258
259    #[test]
260    fn evicts_longest_run_first() {
261        // Lines 42-68: runs are segmented, sorted longest-first, and each chunk is
262        // flushed as-is in ascending offset order within a run.
263        let sink = RecordingSink::new();
264        let mut p = CacheDirectPusher::new(sink.clone(), 70, 0);
265        // runB (length 40)
266        p.push(&(100..120), bb(&"B".repeat(20))).unwrap();
267        p.push(&(120..140), bb(&"B".repeat(20))).unwrap();
268        // runC (length 10)
269        p.push(&(200..210), bb(&"C".repeat(10))).unwrap();
270        // runA (length 20, split so the trigger crosses the watermark mid-run)
271        p.push(&(0..10), bb(&"A".repeat(10))).unwrap();
272        p.push(&(10..20), bb(&"A".repeat(10))).unwrap();
273        // The final 10 bytes of runA arrive after the eviction.
274        p.push(&(20..30), bb(&"A".repeat(10))).unwrap();
275        p.flush().unwrap();
276
277        let pushes = sink.pushes.lock().unwrap();
278        // Longest run (runB, length 40) must be flushed first.
279        assert_eq!(pushes[0].0, 100..120);
280        assert_eq!(pushes.len(), 6);
281        drop(pushes);
282    }
283
284    #[test]
285    fn duplicate_start_overwrites_old_bytes() {
286        // Lines 107-110: re-inserting at an already-cached start replaces the old
287        // bytes and keeps `cache_size` correct.
288        let sink = RecordingSink::new();
289        let mut p = CacheDirectPusher::new(sink.clone(), 100, 0);
290        p.push(&(0..10), bb(&"A".repeat(10))).unwrap();
291        p.push(&(0..10), bb(&"B".repeat(10))).unwrap();
292        p.flush().unwrap();
293        let pushes = sink.pushes.lock().unwrap();
294        assert_eq!(pushes.len(), 1);
295        assert_eq!(&pushes[0].1[..], b"BBBBBBBBBB");
296        drop(pushes);
297    }
298
299    #[test]
300    fn inner_push_failure_rebuffers_partial() {
301        // Lines 77-84: when an inner push fails, the un-written tail is re-inserted
302        // into the cache and the error is returned to the caller.
303        let sink = RecordingSink::new();
304        sink.fail_next.store(true, Ordering::SeqCst);
305        let mut p = CacheDirectPusher::new(sink, 10, 0);
306        let res = p.push(&(0..10), bb(&"A".repeat(10)));
307        assert!(res.is_err());
308    }
309
310    #[test]
311    fn evict_stops_early_at_low_watermark() {
312        // Lines 87-89: once the buffered size drops to <= low_watermark, the
313        // eviction loop breaks early and the remaining (separate) run stays buffered.
314        // The three chunks are kept as separate runs via gaps.
315        let sink = RecordingSink::new();
316        let mut p = CacheDirectPusher::new(sink.clone(), 100, 30);
317        p.push(&(0..60), bb(&"A".repeat(60))).unwrap();
318        p.push(&(100..120), bb(&"B".repeat(20))).unwrap();
319        p.push(&(200..220), bb(&"C".repeat(20))).unwrap();
320
321        let pushes = sink.pushes.lock().unwrap();
322        // runA is flushed; runB and runC are retained because the cache is already
323        // at/below the low watermark after runA.
324        assert_eq!(pushes.len(), 2);
325        assert_eq!(pushes[0].0, 0..60);
326        assert_eq!(pushes[1].0, 100..120);
327        drop(pushes);
328    }
329
330    #[test]
331    fn set_listener_forwards_to_inner() {
332        // Lines 98-99: the listener is forwarded to the inner pusher.
333        let sink = RecordingSink::new();
334        let mut p = CacheDirectPusher::new(sink.clone(), 100, 0);
335        p.set_listener(Box::new(|_| {}));
336        assert!(sink.listener_set.load(Ordering::SeqCst));
337    }
338
339    #[test]
340    fn direct_evict_partial_write_rebuffers_chunk_tail_at_correct_offset() {
341        // A run of three 10-byte chunks [0..30) is evicted chunk-by-chunk; a partial
342        // inner write on the first chunk (first 2 bytes persisted, tail returned) must
343        // re-buffer that chunk's 8-byte tail at offset 2 and retry it on flush.
344        let sink = PartialSinkDirect {
345            pushes: Arc::new(Mutex::new(Vec::new())),
346            partial: Arc::new(AtomicBool::new(false)),
347        };
348        let mut p = CacheDirectPusher::new(sink.clone(), 30, 0);
349        p.push(&(0..10), bb(&"A".repeat(10))).unwrap();
350        p.push(&(10..20), bb(&"B".repeat(10))).unwrap();
351        let res = p.push(&(20..30), bb(&"C".repeat(10)));
352        assert!(res.is_err());
353        p.flush().unwrap();
354        let pushes = sink.pushes.lock().unwrap();
355        // First chunk handed to inner, then its tail retried at [2..10), then the rest.
356        assert_eq!(pushes.len(), 4);
357        assert_eq!(pushes[0].0, 0..10);
358        assert_eq!(pushes[1].0, 2..10);
359        assert_eq!(&pushes[1].1[..], b"AAAAAAAA");
360        assert_eq!(pushes[2].0, 10..20);
361        assert_eq!(pushes[3].0, 20..30);
362    }
363}