Skip to main content

fast_pull/cache/
buf_writer.rs

1//! A `std::io::BufWriter`-style contiguous write buffer for
2//! [`Pusher`](crate::Pusher)s.
3
4use crate::{ProgressEntry, ProgressListener, Pusher};
5use bytes::{Bytes, BytesMut};
6
7/// Pusher decorator that provides `std::io::BufWriter`-style linear write buffering.
8///
9/// Unlike the other buffers in this module (`direct`, `merge`, `seq`), which key
10/// chunks by `range.start` in a `BTreeMap` to absorb out-of-order writes, this
11/// decorator mimics the fixed-size, sequential buffer (backed by `BytesMut`) of
12/// `std::io::BufWriter`: it coalesces *contiguous* writes into a single `push` to
13/// the inner pusher, flushing when the buffer is full or when an incoming chunk is
14/// not contiguous with the buffered run.
15///
16/// This keeps the whole write chain in the `Pusher` abstraction: any `Pusher` (for
17/// example a raw file sink such as `crate::StdFilePusher`) can be
18/// wrapped to gain syscall / inner-call batching without depending on
19/// `std::io::BufWriter`.
20///
21/// # Position tracking
22/// The decorator derives the logical next-write position as `run_start + buf.len()`
23/// to detect contiguous vs. seeked writes, exactly like `StdFilePusher::write_at`. On a
24/// non-contiguous write, the buffered run is flushed first and a new run is started
25/// at the new offset.
26///
27/// # Error semantics
28/// On an inner `push` failure during flush, the buffered run is retained internally
29/// for retry and the incoming `bytes` are handed back as `Err((e, bytes))` so the
30/// caller can retry them; this mirrors the other decorators in this module, which
31/// keep failed data internally rather than dropping it.
32#[derive(Debug)]
33pub struct BufWriterPusher<P> {
34    inner: P,
35    buf: BytesMut,
36    capacity: usize,
37    /// Start offset of the currently buffered contiguous run. The next-write
38    /// position is always `run_start + buf.len()` while a run is buffered.
39    run_start: u64,
40}
41
42impl<P: Pusher> BufWriterPusher<P> {
43    /// Build a buffered pusher with the given inner sink and buffer capacity.
44    #[must_use]
45    pub fn new(inner: P, capacity: usize) -> Self {
46        Self {
47            inner,
48            buf: BytesMut::with_capacity(capacity),
49            capacity,
50            run_start: 0,
51        }
52    }
53
54    /// Flush the currently buffered run to the inner pusher, if any.
55    ///
56    /// On an inner failure the unwritten tail is kept in `self.buf` and `Err(e)`
57    /// is returned (data is still held internally for the next flush).
58    fn flush_buf(&mut self) -> Result<(), P::Error> {
59        if self.buf.is_empty() {
60            return Ok(());
61        }
62        let start = self.run_start;
63        let len = self.buf.len();
64        // `BytesMut::split()` yields the filled prefix as a `Bytes` (O(1)) and
65        // leaves `self.buf` empty but capacity-retained for the next run.
66        let chunk: Bytes = self.buf.split().freeze();
67        match self.inner.push(&(start..start + len as u64), chunk) {
68            Ok(()) => Ok(()),
69            Err((e, rem)) => {
70                let written = len.saturating_sub(rem.len());
71                self.buf.extend_from_slice(&rem);
72                self.run_start = start + written as u64;
73                Err(e)
74            }
75        }
76    }
77}
78
79impl<P: Pusher> Pusher for BufWriterPusher<P> {
80    type Error = P::Error;
81
82    fn set_listener(&mut self, cb: ProgressListener) {
83        self.inner.set_listener(cb);
84    }
85
86    fn push(&mut self, range: &ProgressEntry, bytes: Bytes) -> Result<(), (Self::Error, Bytes)> {
87        if bytes.is_empty() {
88            return Ok(());
89        }
90
91        // Flush if the buffer is non-empty and the incoming chunk is either
92        // non-contiguous or would overflow the fixed capacity.
93        if !self.buf.is_empty()
94            && (range.start != self.run_start + self.buf.len() as u64
95                || self.buf.len() + bytes.len() > self.capacity)
96            && let Err(e) = self.flush_buf()
97        {
98            // We still hold the existing buffered run; the caller's bytes were
99            // not accepted, so hand them back for retry.
100            return Err((e, bytes));
101        }
102
103        if self.buf.is_empty() {
104            // Start a fresh contiguous run at this offset.
105            self.run_start = range.start;
106        }
107
108        // Large writes bypass the buffer, matching `BufWriter`'s behaviour.
109        if bytes.len() >= self.capacity {
110            return self.inner.push(range, bytes);
111        }
112
113        self.buf.extend_from_slice(bytes.as_ref());
114        Ok(())
115    }
116
117    fn flush(&mut self) -> Result<(), Self::Error> {
118        self.flush_buf()?;
119        self.inner.flush()
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    #![allow(clippy::unwrap_used)]
126    use super::*;
127    use std::sync::{Arc, Mutex};
128
129    /// Shared log of recorded pushes: offset range + bytes.
130    type PushLog = Arc<Mutex<Vec<(u64, u64, Vec<u8>)>>>;
131
132    /// Inner pusher that records every `push` it receives.
133    #[derive(Clone, Debug, Default)]
134    struct RecordingPusher {
135        log: PushLog,
136    }
137    impl Pusher for RecordingPusher {
138        type Error = std::io::Error;
139        fn push(
140            &mut self,
141            range: &ProgressEntry,
142            bytes: Bytes,
143        ) -> Result<(), (Self::Error, Bytes)> {
144            self.log
145                .lock()
146                .unwrap()
147                .push((range.start, range.end, bytes.to_vec()));
148            Ok(())
149        }
150    }
151
152    /// Inner pusher that fails the first `push`, then succeeds on every retry.
153    #[derive(Clone, Debug)]
154    struct FlakyPusher {
155        log: PushLog,
156        did_fail: Arc<std::sync::atomic::AtomicBool>,
157    }
158    impl Pusher for FlakyPusher {
159        type Error = std::io::Error;
160        fn push(
161            &mut self,
162            range: &ProgressEntry,
163            bytes: Bytes,
164        ) -> Result<(), (Self::Error, Bytes)> {
165            if self.did_fail.load(std::sync::atomic::Ordering::Relaxed) {
166                self.log
167                    .lock()
168                    .unwrap()
169                    .push((range.start, range.end, bytes.to_vec()));
170                return Ok(());
171            }
172            self.did_fail
173                .store(true, std::sync::atomic::Ordering::Relaxed);
174            Err((std::io::Error::other("boom"), bytes))
175        }
176    }
177
178    /// Inner pusher that writes only the first 2 bytes of the chunk on its first call,
179    /// then fails returning the unwritten tail as `rem`. Exercises partial-write handling.
180    #[derive(Clone, Debug)]
181    struct PartialPusher {
182        log: PushLog,
183        wrote_partial: Arc<std::sync::atomic::AtomicBool>,
184    }
185    impl Pusher for PartialPusher {
186        type Error = std::io::Error;
187        fn push(
188            &mut self,
189            range: &ProgressEntry,
190            bytes: Bytes,
191        ) -> Result<(), (Self::Error, Bytes)> {
192            self.log
193                .lock()
194                .unwrap()
195                .push((range.start, range.end, bytes.to_vec()));
196            if !self
197                .wrote_partial
198                .swap(true, std::sync::atomic::Ordering::Relaxed)
199            {
200                // Pretend we persisted the first 2 bytes, then failed with the tail left over.
201                let rem = bytes.slice(2..);
202                return Err((std::io::Error::other("partial"), rem));
203            }
204            Ok(())
205        }
206    }
207
208    /// Inner pusher that records every `set_listener` call and fires the listener on each
209    /// successful `push`, so the test can observe whether `BufWriterPusher` forwards it.
210    #[derive(Default)]
211    struct ListenerRecordingPusher {
212        fired: Arc<Mutex<Vec<(u64, u64)>>>,
213        listener: Option<ProgressListener>,
214    }
215    impl Pusher for ListenerRecordingPusher {
216        type Error = std::io::Error;
217        fn set_listener(&mut self, cb: ProgressListener) {
218            self.listener = Some(cb);
219        }
220        fn push(
221            &mut self,
222            range: &ProgressEntry,
223            _bytes: Bytes,
224        ) -> Result<(), (Self::Error, Bytes)> {
225            if let Some(cb) = &mut self.listener {
226                cb(range.clone());
227            }
228            self.fired.lock().unwrap().push((range.start, range.end));
229            Ok(())
230        }
231    }
232
233    #[test]
234    fn contiguous_writes_coalesce_into_one_push() {
235        let log = Arc::new(Mutex::new(Vec::new()));
236        let mut bp = BufWriterPusher::new(RecordingPusher { log: log.clone() }, 1024);
237        bp.push(&(0..4), Bytes::from_static(b"abcd")).unwrap();
238        bp.push(&(4..8), Bytes::from_static(b"efgh")).unwrap();
239        bp.flush().unwrap();
240
241        let l = log.lock().unwrap();
242        assert_eq!(l.len(), 1, "expected a single coalesced push");
243        assert_eq!(l[0], (0, 8, b"abcdefgh".to_vec()));
244        drop(l);
245    }
246
247    #[test]
248    fn noncontiguous_write_flushes_existing_run() {
249        let log = Arc::new(Mutex::new(Vec::new()));
250        let mut bp = BufWriterPusher::new(RecordingPusher { log: log.clone() }, 1024);
251        bp.push(&(0..4), Bytes::from_static(b"abcd")).unwrap();
252        // Non-contiguous: forces a flush of [0..4) then starts a new run.
253        bp.push(&(10..14), Bytes::from_static(b"efgh")).unwrap();
254        bp.flush().unwrap();
255
256        let l = log.lock().unwrap();
257        assert_eq!(l.len(), 2);
258        assert_eq!(l[0], (0, 4, b"abcd".to_vec()));
259        assert_eq!(l[1], (10, 14, b"efgh".to_vec()));
260        drop(l);
261    }
262
263    #[test]
264    fn capacity_overflow_flushes() {
265        // Capacity 4: the second (contiguous) write overflows and flushes [0..4).
266        let log = Arc::new(Mutex::new(Vec::new()));
267        let mut bp = BufWriterPusher::new(RecordingPusher { log: log.clone() }, 4);
268        bp.push(&(0..4), Bytes::from_static(b"abcd")).unwrap();
269        bp.push(&(4..8), Bytes::from_static(b"efgh")).unwrap();
270        bp.flush().unwrap();
271
272        let l = log.lock().unwrap();
273        assert_eq!(l.len(), 2);
274        assert_eq!(l[0], (0, 4, b"abcd".to_vec()));
275        assert_eq!(l[1], (4, 8, b"efgh".to_vec()));
276        drop(l);
277    }
278
279    #[test]
280    fn large_write_bypasses_buffer() {
281        let log = Arc::new(Mutex::new(Vec::new()));
282        // Capacity 4, but write 8 bytes directly -> should bypass buffering.
283        let mut bp = BufWriterPusher::new(RecordingPusher { log: log.clone() }, 4);
284        bp.push(&(0..8), Bytes::from_static(b"abcdefgh")).unwrap();
285        bp.flush().unwrap();
286
287        let l = log.lock().unwrap();
288        assert_eq!(l.len(), 1);
289        assert_eq!(l[0], (0, 8, b"abcdefgh".to_vec()));
290        drop(l);
291    }
292
293    #[test]
294    fn random_access_write_is_correct_with_mem_pusher() {
295        let mem = crate::MemPusher::with_capacity(16);
296        let mut bp = BufWriterPusher::new(mem, 8 * 1024);
297        bp.push(&(2..5), Bytes::from_static(b"234")).unwrap();
298        bp.flush().unwrap();
299
300        let content = bp.inner.receive.lock().clone();
301        // `MemPusher` grows rather than pre-sizing, so a write at [2..5) yields 5 bytes.
302        assert_eq!(content, b"\0\x00234");
303    }
304
305    #[test]
306    fn failed_inner_push_is_retained_and_retried() {
307        let log = Arc::new(Mutex::new(Vec::new()));
308        let mut bp = BufWriterPusher::new(
309            FlakyPusher {
310                log: log.clone(),
311                did_fail: Arc::new(std::sync::atomic::AtomicBool::new(false)),
312            },
313            1024,
314        );
315
316        bp.push(&(0..4), Bytes::from_static(b"abcd")).unwrap();
317        // First flush hits the failing inner push; data is retained internally.
318        assert!(bp.flush().is_err());
319        // Next flush retries and succeeds.
320        bp.flush().unwrap();
321
322        let l = log.lock().unwrap();
323        assert_eq!(l.len(), 1);
324        assert_eq!(l[0], (0, 4, b"abcd".to_vec()));
325        drop(l);
326    }
327
328    #[test]
329    fn empty_push_is_a_noop() {
330        // Line 89: a zero-length chunk returns `Ok(())` without touching the buffer.
331        let log = Arc::new(Mutex::new(Vec::new()));
332        let mut bp = BufWriterPusher::new(RecordingPusher { log: log.clone() }, 1024);
333        bp.push(&(0..0), Bytes::new()).unwrap();
334        assert!(log.lock().unwrap().is_empty());
335    }
336
337    #[test]
338    fn flush_failure_during_push_returns_caller_bytes() {
339        // Lines 97-101: a non-contiguous write while the buffer is non-empty triggers
340        // an inner flush; when that flush fails, the caller's bytes are handed back.
341        let log = Arc::new(Mutex::new(Vec::new()));
342        let mut bp = BufWriterPusher::new(
343            FlakyPusher {
344                log,
345                did_fail: Arc::new(std::sync::atomic::AtomicBool::new(false)),
346            },
347            1024,
348        );
349        // Buffered run [0..4); the FlakyPusher fails its first inner push.
350        bp.push(&(0..4), Bytes::from_static(b"abcd")).unwrap();
351        // Non-contiguous write forces a flush of [0..4), which fails; the incoming
352        // bytes [10..14) are returned to the caller.
353        let res = bp.push(&(10..14), Bytes::from_static(b"efgh"));
354        assert!(res.is_err());
355        let (_e, remaining) = res.unwrap_err();
356        assert_eq!(&remaining[..], b"efgh");
357    }
358
359    #[test]
360    fn flush_buf_partial_write_is_retained_and_retried() {
361        // A partial inner write (first 2 of 4 bytes persisted, then failure with the
362        // 2-byte tail as `rem`) must leave exactly that tail buffered for retry.
363        let log = Arc::new(Mutex::new(Vec::new()));
364        let mut bp = BufWriterPusher::new(
365            PartialPusher {
366                log: log.clone(),
367                wrote_partial: Arc::new(std::sync::atomic::AtomicBool::new(false)),
368            },
369            1024,
370        );
371        bp.push(&(0..4), Bytes::from_static(b"abcd")).unwrap();
372        // First flush hits the partial failure; the 2-byte tail stays in `self.buf`.
373        assert!(bp.flush().is_err());
374        // Retry flushes only the retained tail.
375        bp.flush().unwrap();
376
377        let l = log.lock().unwrap();
378        // The inner saw the full 4-byte chunk first, then the 2-byte tail on retry.
379        assert_eq!(l.len(), 2);
380        assert_eq!(l[0], (0, 4, b"abcd".to_vec()));
381        assert_eq!(l[1], (2, 4, b"cd".to_vec()));
382    }
383
384    #[test]
385    fn buf_writer_forwards_set_listener_and_fires_on_flush() {
386        let sink = ListenerRecordingPusher::default();
387        let fired = sink.fired.clone();
388        let mut bp = BufWriterPusher::new(sink, 1024);
389        bp.set_listener(Box::new(|_r: ProgressEntry| {}));
390        bp.push(&(0..4), Bytes::from_static(b"abcd")).unwrap();
391        // A buffered write has not reached the inner sink yet, so no listener fired.
392        assert!(
393            fired.lock().unwrap().is_empty(),
394            "buffered write must not reach the inner listener before flush"
395        );
396        bp.flush().unwrap();
397        // On flush the coalesced range reaches the inner sink and its listener fires.
398        assert_eq!(fired.lock().unwrap().as_slice(), &[(0, 4)]);
399    }
400
401    #[test]
402    fn capacity_full_does_not_flush_prematurely() {
403        // Capacity 4: two contiguous 2-byte writes fill the buffer exactly. Reaching
404        // `== capacity` must NOT trigger a flush; only `> capacity` does.
405        let log = Arc::new(Mutex::new(Vec::new()));
406        let mut bp = BufWriterPusher::new(RecordingPusher { log: log.clone() }, 4);
407        bp.push(&(0..2), Bytes::from_static(b"ab")).unwrap();
408        bp.push(&(2..4), Bytes::from_static(b"cd")).unwrap();
409        assert!(
410            log.lock().unwrap().is_empty(),
411            "reaching exactly capacity must not flush"
412        );
413        bp.flush().unwrap();
414        let l = log.lock().unwrap();
415        assert_eq!(l.len(), 1);
416        assert_eq!(l[0], (0, 4, b"abcd".to_vec()));
417    }
418
419    #[test]
420    fn failed_noncontiguous_flush_keeps_old_run_for_retry() {
421        // A non-contiguous write forces a flush of the buffered [0..4) run; when that
422        // flush fails, the caller's bytes are returned AND the old run is kept so it can
423        // be retried on the next flush.
424        let log = Arc::new(Mutex::new(Vec::new()));
425        let mut bp = BufWriterPusher::new(
426            FlakyPusher {
427                log: log.clone(),
428                did_fail: Arc::new(std::sync::atomic::AtomicBool::new(false)),
429            },
430            1024,
431        );
432        bp.push(&(0..4), Bytes::from_static(b"abcd")).unwrap();
433        let res = bp.push(&(10..14), Bytes::from_static(b"efgh"));
434        assert!(res.is_err());
435        // The old run must still be retryable.
436        bp.flush().unwrap();
437        let l = log.lock().unwrap();
438        assert_eq!(l.len(), 1);
439        assert_eq!(l[0], (0, 4, b"abcd".to_vec()));
440    }
441}