junction_core/
error.rs

1use std::{borrow::Cow, net::SocketAddr, time::Instant};
2
3use junction_api::{backend::BackendId, http::Route, Name};
4use smol_str::{SmolStr, ToSmolStr};
5
6#[derive(Clone, Debug)]
7pub(crate) struct Trace {
8    start: Instant,
9    phase: TracePhase,
10    events: Vec<TraceEvent>,
11}
12
13#[derive(Clone, Debug)]
14pub(crate) struct TraceEvent {
15    pub(crate) phase: TracePhase,
16    pub(crate) kind: TraceEventKind,
17    pub(crate) at: Instant,
18    pub(crate) kv: Vec<TraceData>,
19}
20
21type TraceData = (&'static str, SmolStr);
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub(crate) enum TracePhase {
25    RouteResolution,
26    EndpointSelection(u8),
27}
28
29#[derive(Clone, Copy, Debug)]
30pub(crate) enum TraceEventKind {
31    RouteLookup,
32    RouteRuleMatched,
33    BackendSelected,
34    BackendLookup,
35    EndpointsLookup,
36    SelectAddr,
37}
38
39impl Trace {
40    pub(crate) fn new() -> Self {
41        Trace {
42            start: Instant::now(),
43            phase: TracePhase::RouteResolution,
44            events: Vec::new(),
45        }
46    }
47
48    pub(crate) fn events(&self) -> impl Iterator<Item = &TraceEvent> {
49        self.events.iter()
50    }
51
52    pub(crate) fn start(&self) -> Instant {
53        self.start
54    }
55
56    pub(crate) fn lookup_route(&mut self, route: &Route) {
57        debug_assert!(matches!(self.phase, TracePhase::RouteResolution));
58
59        self.events.push(TraceEvent {
60            kind: TraceEventKind::RouteLookup,
61            phase: TracePhase::RouteResolution,
62            at: Instant::now(),
63            kv: vec![("route", route.id.to_smolstr())],
64        })
65    }
66
67    pub(crate) fn matched_rule(&mut self, rule: usize, rule_name: Option<&Name>) {
68        debug_assert!(matches!(self.phase, TracePhase::RouteResolution));
69
70        let kv = match rule_name {
71            Some(name) => vec![("rule-name", name.to_smolstr())],
72            None => vec![("rule-idx", rule.to_smolstr())],
73        };
74
75        self.events.push(TraceEvent {
76            kind: TraceEventKind::RouteRuleMatched,
77            phase: TracePhase::RouteResolution,
78            at: Instant::now(),
79            kv,
80        })
81    }
82
83    pub(crate) fn select_backend(&mut self, backend: &BackendId) {
84        debug_assert!(matches!(self.phase, TracePhase::RouteResolution));
85
86        self.events.push(TraceEvent {
87            phase: self.phase,
88            kind: TraceEventKind::BackendSelected,
89            at: Instant::now(),
90            kv: vec![("name", backend.to_smolstr())],
91        });
92    }
93
94    pub(crate) fn start_endpoint_selection(&mut self) {
95        let next_phase = match self.phase {
96            TracePhase::RouteResolution => TracePhase::EndpointSelection(0),
97            TracePhase::EndpointSelection(n) => TracePhase::EndpointSelection(n + 1),
98        };
99        self.phase = next_phase;
100    }
101
102    pub(crate) fn lookup_backend(&mut self, backend: &BackendId) {
103        debug_assert!(matches!(self.phase, TracePhase::EndpointSelection(_)));
104
105        self.events.push(TraceEvent {
106            kind: TraceEventKind::BackendLookup,
107            phase: self.phase,
108            at: Instant::now(),
109            kv: vec![("backend-id", backend.to_smolstr())],
110        })
111    }
112
113    pub(crate) fn lookup_endpoints(&mut self, backend: &BackendId) {
114        debug_assert!(matches!(self.phase, TracePhase::EndpointSelection(_)));
115
116        self.events.push(TraceEvent {
117            kind: TraceEventKind::EndpointsLookup,
118            phase: self.phase,
119            at: Instant::now(),
120            kv: vec![("backend-id", backend.to_smolstr())],
121        })
122    }
123
124    pub(crate) fn load_balance(
125        &mut self,
126        lb_name: &'static str,
127        addr: Option<&SocketAddr>,
128        extra: Vec<TraceData>,
129    ) {
130        debug_assert!(matches!(self.phase, TracePhase::EndpointSelection(_)));
131
132        let mut kv = Vec::with_capacity(extra.len() + 2);
133        kv.push(("type", lb_name.to_smolstr()));
134        kv.push((
135            "addr",
136            addr.map(|a| a.to_smolstr())
137                .unwrap_or_else(|| "-".to_smolstr()),
138        ));
139        kv.extend(extra);
140
141        self.events.push(TraceEvent {
142            kind: TraceEventKind::SelectAddr,
143            phase: self.phase,
144            at: Instant::now(),
145            kv,
146        });
147    }
148}
149
150/// A `Result` alias where the `Err` case is `junction_core::Error`.
151pub type Result<T> = std::result::Result<T, Error>;
152
153/// An error when using the Junction client.
154#[derive(Debug, thiserror::Error)]
155#[error("{inner}")]
156pub struct Error {
157    // a trace of what's happened so far
158    trace: Option<Trace>,
159
160    // boxed to keep the size of the error down. this apparently has a large
161    // effect on the performance of calls to functions that return
162    // Result<_, Error>.
163    //
164    // https://rust-lang.github.io/rust-clippy/master/index.html#result_large_err
165    // https://docs.rs/serde_json/latest/src/serde_json/error.rs.html#15-20
166    inner: Box<ErrorImpl>,
167}
168
169impl Error {
170    /// Returns `true` if this is a temporary error.
171    ///
172    /// Temporary errors may occur because of a network timeout or because of
173    /// lag fetching a configuration from a Junction server.
174    pub fn is_temporary(&self) -> bool {
175        matches!(*self.inner, ErrorImpl::NoReachableEndpoints { .. })
176    }
177}
178
179impl Error {
180    // timeouts
181
182    pub(crate) fn timed_out(message: &'static str, trace: Trace) -> Self {
183        let inner = ErrorImpl::TimedOut(Cow::from(message));
184        Self {
185            trace: Some(trace),
186            inner: Box::new(inner),
187        }
188    }
189
190    // url problems
191    //
192    // TODO: should this be a separate type? thye don't need a Trace or anything
193
194    pub(crate) fn into_invalid_url(message: String) -> Self {
195        let inner = ErrorImpl::InvalidUrl(Cow::Owned(message));
196        Self {
197            trace: None,
198            inner: Box::new(inner),
199        }
200    }
201
202    pub(crate) fn invalid_url(message: &'static str) -> Self {
203        let inner = ErrorImpl::InvalidUrl(Cow::Borrowed(message));
204        Self {
205            trace: None,
206            inner: Box::new(inner),
207        }
208    }
209
210    // route problems
211
212    pub(crate) fn no_route_matched(authority: String, trace: Trace) -> Self {
213        Self {
214            trace: Some(trace),
215            inner: Box::new(ErrorImpl::NoRouteMatched { authority }),
216        }
217    }
218
219    pub(crate) fn no_rule_matched(route: Name, trace: Trace) -> Self {
220        Self {
221            trace: Some(trace),
222            inner: Box::new(ErrorImpl::NoRuleMatched { route }),
223        }
224    }
225
226    pub(crate) fn invalid_route(
227        message: &'static str,
228        id: Name,
229        rule: usize,
230        trace: Trace,
231    ) -> Self {
232        Self {
233            trace: Some(trace),
234            inner: Box::new(ErrorImpl::InvalidRoute { id, message, rule }),
235        }
236    }
237
238    // backend problems
239
240    pub(crate) fn no_backend(backend: BackendId, trace: Trace) -> Self {
241        Self {
242            trace: Some(trace),
243            inner: Box::new(ErrorImpl::NoBackend { backend }),
244        }
245    }
246
247    pub(crate) fn no_reachable_endpoints(backend: BackendId, trace: Trace) -> Self {
248        Self {
249            trace: Some(trace),
250            inner: Box::new(ErrorImpl::NoReachableEndpoints { backend }),
251        }
252    }
253}
254
255#[derive(Debug, thiserror::Error)]
256enum ErrorImpl {
257    #[error("timed out: {0}")]
258    TimedOut(Cow<'static, str>),
259
260    #[error("invalid url: {0}")]
261    InvalidUrl(Cow<'static, str>),
262
263    #[error("invalid route configuration")]
264    InvalidRoute {
265        message: &'static str,
266        id: Name,
267        rule: usize,
268    },
269
270    #[error("no route matched: '{authority}'")]
271    NoRouteMatched { authority: String },
272
273    #[error("{route}: no rules matched the request")]
274    NoRuleMatched { route: Name },
275
276    #[error("{backend}: backend not found")]
277    NoBackend { backend: BackendId },
278
279    #[error("{backend}: no reachable endpoints")]
280    NoReachableEndpoints { backend: BackendId },
281}