Skip to main content

tycho_simulation/snapshot_feed/
errors.rs

1//! What a feed reports when a snapshot does not arrive.
2
3use thiserror::Error;
4use tycho_common::simulation::errors::SimulationError;
5
6/// Failures of the feed layer: fetching, decoding, and publishing a provider's snapshots.
7///
8/// Match on the variant: it says whether another attempt could answer differently, which is what
9/// a feed's retrying and the conversion to [`SimulationError`] both turn on. The message beside
10/// it is written for a log line, and its wording carries no promise across versions.
11#[derive(Clone, Debug, Error)]
12pub enum FeedError {
13    /// The provider could not be reached, or answered with something a later attempt may not
14    /// answer with: a transport failure, a timeout, a server-side error.
15    #[error("feed connection error: {0}")]
16    Connection(String),
17    /// The provider's answer could not be turned into a snapshot.
18    #[error("feed parsing error: {0}")]
19    Parsing(String),
20    /// No attempt can answer differently, so the feed gives up whatever its failure budget says.
21    /// A failure someone can fix while the feed keeps retrying is not one of these.
22    #[error("feed fatal error: {0}")]
23    Fatal(String),
24    /// The feed was configured with a value it cannot run with.
25    #[error("feed invalid input error: {0}")]
26    InvalidInput(String),
27}
28
29impl FeedError {
30    /// Whether another attempt is pointless. A feed stops on these, whatever failure budget it
31    /// was configured with.
32    pub fn is_fatal(&self) -> bool {
33        matches!(self, FeedError::Fatal(_))
34    }
35
36    /// The same failure with `context` in front of its message: a caller can say what it was
37    /// doing when the failure reached it without reclassifying what went wrong, which stays
38    /// the judgement of whoever found out.
39    pub(crate) fn in_context(self, context: impl std::fmt::Display) -> Self {
40        match self {
41            FeedError::Connection(message) => {
42                FeedError::Connection(format!("{context}: {message}"))
43            }
44            FeedError::Parsing(message) => FeedError::Parsing(format!("{context}: {message}")),
45            FeedError::Fatal(message) => FeedError::Fatal(format!("{context}: {message}")),
46            FeedError::InvalidInput(message) => {
47                FeedError::InvalidInput(format!("{context}: {message}"))
48            }
49        }
50    }
51}
52
53impl From<FeedError> for SimulationError {
54    /// A state that cannot answer from the book it holds reports why in the vocabulary its
55    /// caller acts on: a newer book may serve what this one cannot, so only a genuinely fatal
56    /// feed error marks the component as one to stop simulating. The message moves across
57    /// unchanged — the variant it lands in classifies it, and `SimulationError` says so in its
58    /// own `Display`.
59    fn from(err: FeedError) -> Self {
60        match err {
61            FeedError::Connection(message) | FeedError::Parsing(message) => {
62                SimulationError::RecoverableError(message)
63            }
64            FeedError::Fatal(message) => SimulationError::FatalError(message),
65            FeedError::InvalidInput(message) => SimulationError::InvalidInput(message, None),
66        }
67    }
68}