Skip to main content

fast_pull/base/
pusher.rs

1//! The [`Pusher`](crate::Pusher) trait: an abstraction over a chunked data sink.
2
3use crate::ProgressEntry;
4use bytes::Bytes;
5
6/// A callback invoked whenever a chunk has been successfully written to its
7/// destination (disk / memory).
8///
9/// Leaf sinks (`StdFilePusher`, `MmapFilePusher`, `MemPusher`) store this and
10/// call it from their [`Pusher::push`] success path. The alias exists so the
11/// `Box<dyn Fn ...>` parameter does not trip `clippy::type_complexity`.
12pub type ProgressListener = Box<dyn FnMut(ProgressEntry) + Send + 'static>;
13
14/// Abstraction over a data sink that receives pushed byte chunks.
15///
16/// The pusher writes data to its destination and can optionally flush.
17pub trait Pusher: Send + 'static {
18    /// Error produced while pushing.
19    type Error: std::error::Error + Send + Sync + Unpin + 'static;
20    /// Write `content` covering the given `range` to the destination.
21    ///
22    /// On success returns `Ok(())`. On failure returns `Err((error, bytes))`
23    /// where `bytes` is the (possibly partial) payload that was **not** written,
24    /// so the engine can retry it. Implementors should keep already-written
25    /// bytes internally on failure rather than dropping them.
26    #[allow(clippy::missing_errors_doc)]
27    fn push(&mut self, range: &ProgressEntry, content: Bytes) -> Result<(), (Self::Error, Bytes)>;
28    /// Flush any buffered data to the destination.
29    ///
30    /// The default implementation is a no-op. File-backed pushers use this to
31    /// issue `fsync` / `flush` on the underlying file.
32    #[allow(clippy::missing_errors_doc)]
33    fn flush(&mut self) -> Result<(), Self::Error> {
34        Ok(())
35    }
36    /// Install a callback that fires whenever a chunk has been successfully
37    /// pushed to its destination.
38    ///
39    /// The default implementation is a no-op. Leaf sinks override this to store
40    /// the callback; every wrapper **must** override this to forward it to its
41    /// inner pusher, otherwise progress events are silently dropped.
42    #[allow(clippy::needless_pass_by_value)]
43    #[allow(unused_variables)]
44    fn set_listener(&mut self, cb: ProgressListener) {}
45}
46
47/// Marker trait for type-erased error types.
48pub trait AnyError: std::error::Error + Send + Sync + Unpin + 'static {}
49impl<T: std::error::Error + Send + Sync + Unpin + 'static> AnyError for T {}
50
51impl std::error::Error for Box<dyn AnyError> {}
52
53/// A type-erased pusher that boxes both the pusher and its error type.
54///
55/// Useful for FFI boundaries or heterogeneous collections of pushers.
56#[allow(missing_debug_implementations)]
57pub struct BoxPusher {
58    /// The boxed, type-erased inner pusher.
59    pub pusher: Box<dyn Pusher<Error = Box<dyn AnyError>>>,
60}
61impl Pusher for BoxPusher {
62    type Error = Box<dyn AnyError>;
63    fn set_listener(&mut self, cb: ProgressListener) {
64        self.pusher.set_listener(cb);
65    }
66    fn push(&mut self, range: &ProgressEntry, content: Bytes) -> Result<(), (Self::Error, Bytes)> {
67        self.pusher.push(range, content)
68    }
69    fn flush(&mut self) -> Result<(), Self::Error> {
70        self.pusher.flush()
71    }
72}
73
74struct PusherAdapter<P: Pusher> {
75    inner: P,
76}
77impl<P: Pusher> Pusher for PusherAdapter<P> {
78    type Error = Box<dyn AnyError>;
79    fn set_listener(&mut self, cb: ProgressListener) {
80        self.inner.set_listener(cb);
81    }
82    fn push(&mut self, range: &ProgressEntry, content: Bytes) -> Result<(), (Self::Error, Bytes)> {
83        self.inner
84            .push(range, content)
85            .map_err(|(e, b)| (BoxPusher::upcast(e), b))
86    }
87    fn flush(&mut self) -> Result<(), Self::Error> {
88        self.inner.flush().map_err(|e| BoxPusher::upcast(e))
89    }
90}
91
92impl BoxPusher {
93    /// Box a concrete [`Pusher`], erasing its error type.
94    pub fn new<P: Pusher>(pusher: P) -> Self {
95        Self {
96            pusher: Box::new(PusherAdapter { inner: pusher }),
97        }
98    }
99    /// Box an error into the type-erased `AnyError` form used by [`BoxPusher`].
100    pub fn upcast<E: AnyError>(e: E) -> Box<dyn AnyError> {
101        Box::new(e)
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    #![allow(clippy::unwrap_used)]
108    use super::*;
109    use std::sync::atomic::{AtomicBool, Ordering};
110    use std::sync::{Arc, Mutex};
111
112    /// A `Pusher` that does not override the default `set_listener` / `flush`.
113    struct DummyPusher;
114    impl Pusher for DummyPusher {
115        type Error = std::io::Error;
116        fn push(
117            &mut self,
118            _range: &ProgressEntry,
119            _content: Bytes,
120        ) -> Result<(), (Self::Error, Bytes)> {
121            Ok(())
122        }
123    }
124
125    #[test]
126    fn default_set_listener_is_noop() {
127        // Exercises the default `Pusher::set_listener` body (line 42) and the
128        // default-`flush` `DummyPusher::push` path (lines 112-118).
129        let mut p = DummyPusher;
130        p.set_listener(Box::new(|_| {}));
131        p.push(&(0..0), Bytes::new()).unwrap();
132    }
133
134    #[test]
135    fn upcast_boxes_any_error() {
136        // Exercises `BoxPusher::upcast` (lines 96-98).
137        let boxed: Box<dyn AnyError> = BoxPusher::upcast(std::io::Error::other("boom"));
138        let _ = boxed;
139    }
140
141    /// Records every push and listener install; can be told to fail the next
142    /// `push`/`flush` so the `BoxPusher` (and its `PusherAdapter`) error paths
143    /// are exercised.
144    #[derive(Clone)]
145    struct RecordingPusher {
146        pushes: Arc<Mutex<Vec<(ProgressEntry, Bytes)>>>,
147        fail_push: Arc<AtomicBool>,
148        fail_flush: Arc<AtomicBool>,
149        listener_set: Arc<AtomicBool>,
150        listener: Arc<Mutex<Option<ProgressListener>>>,
151    }
152    impl RecordingPusher {
153        fn new() -> Self {
154            Self {
155                pushes: Arc::new(Mutex::new(Vec::new())),
156                fail_push: Arc::new(AtomicBool::new(false)),
157                fail_flush: Arc::new(AtomicBool::new(false)),
158                listener_set: Arc::new(AtomicBool::new(false)),
159                listener: Arc::new(Mutex::new(None)),
160            }
161        }
162    }
163    impl Pusher for RecordingPusher {
164        type Error = std::io::Error;
165        fn set_listener(&mut self, cb: ProgressListener) {
166            self.listener_set.store(true, Ordering::SeqCst);
167            *self.listener.lock().unwrap() = Some(cb);
168        }
169        fn push(
170            &mut self,
171            range: &ProgressEntry,
172            bytes: Bytes,
173        ) -> Result<(), (Self::Error, Bytes)> {
174            if self.fail_push.swap(false, Ordering::SeqCst) {
175                return Err((std::io::Error::other("push"), bytes));
176            }
177            if let Some(cb) = self.listener.lock().unwrap().as_mut() {
178                cb(range.clone());
179            }
180            self.pushes.lock().unwrap().push((range.clone(), bytes));
181            Ok(())
182        }
183        fn flush(&mut self) -> Result<(), Self::Error> {
184            if self.fail_flush.swap(false, Ordering::SeqCst) {
185                Err(std::io::Error::other("flush"))
186            } else {
187                Ok(())
188            }
189        }
190    }
191
192    #[test]
193    fn box_pusher_forwards_push_flush_and_listener() {
194        // Covers `BoxPusher`/`PusherAdapter` success forwarding (lines 61-69, 75-87).
195        let inner = RecordingPusher::new();
196        let mut bp = BoxPusher::new(inner.clone());
197        bp.set_listener(Box::new(|_| {}));
198        assert!(inner.listener_set.load(Ordering::SeqCst));
199        bp.push(&(0..3), Bytes::copy_from_slice(b"abc")).unwrap();
200        bp.flush().unwrap();
201        let pushes = inner.pushes.lock().unwrap();
202        assert_eq!(pushes.len(), 1);
203        assert_eq!(pushes[0].0, 0..3);
204        drop(pushes);
205    }
206
207    #[test]
208    fn box_pusher_upcasts_push_error() {
209        // Covers `PusherAdapter::push`'s `map_err`/upcast path (lines 80-84).
210        let inner = RecordingPusher::new();
211        inner.fail_push.store(true, Ordering::SeqCst);
212        let mut bp = BoxPusher::new(inner);
213        let res = bp.push(&(0..3), Bytes::copy_from_slice(b"abc"));
214        assert!(res.is_err());
215        let _ = res.unwrap_err();
216    }
217
218    #[test]
219    fn box_pusher_upcasts_flush_error() {
220        // Covers `PusherAdapter::flush`'s `map_err`/upcast path (lines 85-87).
221        let inner = RecordingPusher::new();
222        inner.fail_flush.store(true, Ordering::SeqCst);
223        let mut bp = BoxPusher::new(inner);
224        assert!(bp.flush().is_err());
225    }
226
227    #[test]
228    fn box_pusher_listener_fires_on_successful_push() {
229        // End-to-end progress path: the listener is forwarded down to the leaf
230        // sink, and firing it is tied to a successful write, with the correct
231        // range reported.
232        let inner = RecordingPusher::new();
233        let seen = Arc::new(Mutex::new(Vec::<ProgressEntry>::new()));
234        let seen2 = seen.clone();
235        let mut bp = BoxPusher::new(inner);
236        bp.set_listener(Box::new(move |r| seen2.lock().unwrap().push(r)));
237        bp.push(&(0..3), Bytes::copy_from_slice(b"abc")).unwrap();
238        let s = seen.lock().unwrap();
239        assert_eq!(s.len(), 1);
240        assert_eq!(s[0], 0..3);
241    }
242
243    #[test]
244    fn box_pusher_push_error_preserves_unwritten_bytes() {
245        // On failure the unwritten bytes must survive `PusherAdapter`'s `map_err`
246        // untouched; swallowing them would leave the engine unable to retry that
247        // chunk, silently losing data.
248        let inner = RecordingPusher::new();
249        inner.fail_push.store(true, Ordering::SeqCst);
250        let mut bp = BoxPusher::new(inner);
251        let payload = Bytes::copy_from_slice(b"hello");
252        let res = bp.push(&(0..5), payload.clone());
253        let (_, unwritten) = res.unwrap_err();
254        assert_eq!(unwritten, payload);
255    }
256
257    #[test]
258    fn box_pusher_default_flush_is_ok() {
259        // `DummyPusher` does not override `flush`, so the default no-op runs and
260        // must still succeed after being forwarded through `BoxPusher`.
261        let mut bp = BoxPusher::new(DummyPusher);
262        assert!(bp.flush().is_ok());
263    }
264}