Skip to main content

spire_core/worker/
outcome.rs

1use std::convert::Infallible;
2use std::time::Duration;
3
4use crate::request::Tag;
5use crate::{BoxError, Error};
6
7/// What the crawler should do after a worker handled a request.
8#[must_use]
9#[derive(Debug, Default)]
10pub enum Outcome {
11    /// The request is done.
12    #[default]
13    Continue,
14    /// Try the request again, after the given delay or the frontier's
15    /// default delay.
16    Retry {
17        /// Delay before the next attempt; `None` defers to the frontier.
18        after: Option<Duration>,
19    },
20    /// Pause every request in `scope` for `duration`, then try this request
21    /// again. Typically returned on `429 Too Many Requests`.
22    Backoff {
23        /// The requests to pause.
24        scope: Scope,
25        /// How long to pause them.
26        duration: Duration,
27    },
28    /// Stop crawling `scope`: its queued requests are skipped from now on.
29    /// Returned on the first request of a tag, this drops the whole tag.
30    Abort(Scope),
31    /// Stop the crawl: requests in flight finish, nothing new starts.
32    Stop,
33    /// The request failed. Retryable errors are tried again, others are
34    /// recorded as failures.
35    Fail(Error),
36}
37
38impl Outcome {
39    /// Retries the request after the frontier's default delay.
40    pub const fn retry() -> Self {
41        Self::Retry { after: None }
42    }
43
44    /// Retries the request after `delay`.
45    pub const fn retry_after(delay: Duration) -> Self {
46        Self::Retry { after: Some(delay) }
47    }
48
49    /// Pauses requests with the same tag as this one for `duration`.
50    pub const fn backoff(duration: Duration) -> Self {
51        Self::Backoff {
52            scope: Scope::Owner,
53            duration,
54        }
55    }
56
57    /// Skips every further request with the same tag as this one.
58    pub const fn abort() -> Self {
59        Self::Abort(Scope::Owner)
60    }
61
62    /// Skips every further request with `tag`.
63    pub fn abort_tag(tag: impl Into<Tag>) -> Self {
64        Self::Abort(Scope::Tag(tag.into()))
65    }
66
67    /// Pauses requests with `tag` for `duration`.
68    pub fn backoff_tag(tag: impl Into<Tag>, duration: Duration) -> Self {
69        Self::Backoff {
70            scope: Scope::Tag(tag.into()),
71            duration,
72        }
73    }
74}
75
76/// The requests an [`Outcome::Backoff`] or [`Outcome::Abort`] applies to.
77#[derive(Debug, Clone, Default, PartialEq, Eq)]
78pub enum Scope {
79    /// Requests sharing the tag of the request that was handled, including
80    /// untagged requests when it had none.
81    #[default]
82    Owner,
83    /// Requests with the given tag.
84    Tag(Tag),
85}
86
87/// Conversion into an [`Outcome`], implemented for handler return types.
88pub trait IntoOutcome {
89    /// Converts `self` into an [`Outcome`].
90    fn into_outcome(self) -> Outcome;
91}
92
93impl IntoOutcome for Outcome {
94    fn into_outcome(self) -> Outcome {
95        self
96    }
97}
98
99impl IntoOutcome for () {
100    fn into_outcome(self) -> Outcome {
101        Outcome::Continue
102    }
103}
104
105impl IntoOutcome for Infallible {
106    fn into_outcome(self) -> Outcome {
107        match self {}
108    }
109}
110
111impl IntoOutcome for Error {
112    fn into_outcome(self) -> Outcome {
113        Outcome::Fail(self)
114    }
115}
116
117impl IntoOutcome for BoxError {
118    /// Fails with the error, keeping its kind if it is an [`Error`] and
119    /// classifying it as a [`Worker`](crate::ErrorKind::Worker) error
120    /// otherwise.
121    fn into_outcome(self) -> Outcome {
122        Outcome::Fail(Error::from_boxed(crate::ErrorKind::Worker, self))
123    }
124}
125
126impl<T> IntoOutcome for Option<T>
127where
128    T: IntoOutcome,
129{
130    fn into_outcome(self) -> Outcome {
131        self.map_or(Outcome::Continue, IntoOutcome::into_outcome)
132    }
133}
134
135impl<T, E> IntoOutcome for Result<T, E>
136where
137    T: IntoOutcome,
138    E: IntoOutcome,
139{
140    fn into_outcome(self) -> Outcome {
141        match self {
142            Ok(value) => value.into_outcome(),
143            Err(error) => error.into_outcome(),
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use crate::ErrorKind;
152
153    #[test]
154    fn conversions() {
155        assert!(matches!(().into_outcome(), Outcome::Continue));
156        assert!(matches!(None::<Outcome>.into_outcome(), Outcome::Continue));
157
158        let ok: Result<(), Error> = Ok(());
159        assert!(matches!(ok.into_outcome(), Outcome::Continue));
160
161        let err: Result<(), Error> = Err(Error::new(ErrorKind::Http));
162        assert!(matches!(err.into_outcome(), Outcome::Fail(_)));
163
164        let retry: Result<(), Outcome> = Err(Outcome::retry());
165        assert!(matches!(
166            retry.into_outcome(),
167            Outcome::Retry { after: None }
168        ));
169    }
170}