Skip to main content

fast_pull/base/
puller.rs

1//! The [`Puller`](crate::Puller) trait: an abstraction over a chunked data source.
2
3use crate::ProgressEntry;
4use bytes::Bytes;
5use core::time::Duration;
6use futures::TryStream;
7
8/// A pull stream that yields [`Bytes`] chunks.
9///
10/// Each chunk is accompanied by an optional retry delay on error.
11pub trait PullStream<E>:
12    TryStream<Ok = Bytes, Error = (E, Option<Duration>)> + Send + Unpin
13{
14}
15impl<E, T> PullStream<E> for T where
16    T: TryStream<Ok = Bytes, Error = (E, Option<Duration>)> + Send + Unpin
17{
18}
19/// Result type returned by pulling operations.
20///
21/// On error, returns the error alongside an optional retry-after duration.
22pub type PullResult<T, E> = Result<T, (E, Option<Duration>)>;
23
24/// Abstraction over a data source that can be pulled (downloaded) in chunks.
25///
26/// Implementors produce a [`PullStream`] of bytes, optionally restricted to a
27/// specific byte range. Cloning is required for retry and work-stealing scenarios.
28pub trait Puller: Send + Sync + Clone + 'static {
29    /// Error produced while pulling.
30    type Error: PullerError;
31    /// Pull a (sub)range of the source as a stream of byte chunks.
32    ///
33    /// Passing `None` for `range` requests the entire source. The returned
34    /// [`PullStream`] yields [`Bytes`] chunks; each error carries an optional
35    /// retry delay that the engine honors via its retry backoff. Implementors
36    /// must be `Clone` so workers can be spawned and work can be stolen/retried.
37    fn pull(
38        &mut self,
39        range: Option<&ProgressEntry>,
40    ) -> impl Future<Output = PullResult<impl PullStream<Self::Error>, Self::Error>> + Send;
41}
42
43/// Extension trait for pull errors, distinguishing recoverable from irrecoverable failures.
44pub trait PullerError: std::error::Error + Send + Sync + Unpin + 'static {
45    /// Whether an error is fatal and must **not** be retried.
46    ///
47    /// The default (`false`) means the error is recoverable and the engine will
48    /// retry after the configured backoff. Implementors **must** override this to
49    /// return `true` for fatal errors: forgetting to do so lets the engine retry
50    /// indefinitely until its backoff gives up.
51    fn is_irrecoverable(&self) -> bool {
52        false
53    }
54}
55
56impl PullerError for std::convert::Infallible {
57    fn is_irrecoverable(&self) -> bool {
58        #[allow(clippy::uninhabited_references)]
59        match *self {}
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    #![allow(clippy::unwrap_used)]
66    use super::*;
67
68    /// A `PullerError` that relies on the default `is_irrecoverable` impl.
69    #[derive(Debug)]
70    struct DefaultErr;
71    impl std::fmt::Display for DefaultErr {
72        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73            f.write_str("default error")
74        }
75    }
76    impl std::error::Error for DefaultErr {}
77    impl PullerError for DefaultErr {}
78
79    #[test]
80    fn default_is_irrecoverable_is_false() {
81        // Exercises the default `PullerError::is_irrecoverable` body (lines 49-51) and
82        // the `Display` impl for `DefaultErr` (lines 70-72).
83        assert!(!DefaultErr.is_irrecoverable());
84        assert_eq!(format!("{DefaultErr}"), "default error");
85    }
86
87    /// A `PullerError` that overrides `is_irrecoverable` to report a fatal error.
88    #[derive(Debug)]
89    struct FatalErr;
90    impl std::fmt::Display for FatalErr {
91        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92            f.write_str("fatal error")
93        }
94    }
95    impl std::error::Error for FatalErr {}
96    impl PullerError for FatalErr {
97        fn is_irrecoverable(&self) -> bool {
98            true
99        }
100    }
101
102    #[test]
103    fn override_is_irrecoverable_is_true() {
104        // An error that overrides the method reports `true`, which is how the
105        // engine learns to stop retrying. Pinned alongside the default `false`.
106        assert!(FatalErr.is_irrecoverable());
107        assert!(!DefaultErr.is_irrecoverable());
108    }
109
110    #[test]
111    fn irrecoverable_contract_allows_dynamic_decision() {
112        // The answer may depend on the error's own state; it is not required to
113        // be a compile-time constant per type.
114        #[derive(Debug)]
115        struct StatefulErr {
116            fatal: bool,
117        }
118        impl std::fmt::Display for StatefulErr {
119            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120                f.write_str("stateful")
121            }
122        }
123        impl std::error::Error for StatefulErr {}
124        impl PullerError for StatefulErr {
125            fn is_irrecoverable(&self) -> bool {
126                self.fatal
127            }
128        }
129        assert!(!StatefulErr { fatal: false }.is_irrecoverable());
130        assert!(StatefulErr { fatal: true }.is_irrecoverable());
131    }
132}