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