tag2upload_service_manager/
error.rs

1
2use crate::prelude::*;
3use std::backtrace::Backtrace;
4
5#[derive(Error, Debug)]
6pub enum ProcessingError {
7    #[error("problem at forge: {0:#}")]
8    Forge(anyhow::Error),
9
10    #[error("local problem {0:#}")]
11    Local(anyhow::Error),
12
13    #[error("{0}")]
14    Mismatch(#[from] MismatchError),
15
16    #[error("{0}")]
17    Internal(#[from] InternalError),
18}
19
20/// Token indicating that we are OK to shut this task down
21///
22/// Returned by the closure passed to [`Globals::spawn-task`].
23///
24/// Not an error.
25pub struct TaskWorkComplete {}
26
27/// Not an `Error`
28#[derive(Debug, From)]
29pub enum QuitTask {
30    Crashed(InternalError),
31    Shutdown(ShuttingDown),
32}
33
34pub type TaskResult = Result<TaskWorkComplete, QuitTask>;
35
36#[derive(Error, Debug)]
37pub enum StartupError {
38    #[error("failed to parse/deserialise t2u configuration: {0}")]
39    ParseConfig(figment::Error),
40
41    #[error("invalid configuration")]
42    InvalidConfig,
43
44    #[error("problem with temp directory: {0:#}")]
45    TempDir(AE),
46
47    #[error("failed to initialise http client: {0}")]
48    Reqwest(#[from] reqwest::Error),
49
50    #[error("failed to initialise DNS resolver: {0}")]
51    Resolver(#[from] hickory_resolver::ResolveError),
52
53    #[error("failed to initialise Rocket http server: {0}")]
54    Rocket(#[from] rocket::Error),
55
56    #[error("failed to initialise worker listener(s): {0:#}")]
57    WorkerListener(AE),
58
59    #[error("failed to open database: {0}")]
60    DbOpen(rusqlite::Error),
61
62    #[error("failed to idempotently initialise db schema: {0}")]
63    ExecuteSchema(rusqlite::Error),
64
65    #[error("failed to access database during startup: {0}")]
66    DbAccess(rusqlite::Error),
67
68    #[error("failed to initialise logging: {0:#}")]
69    Logging(AE),
70
71    #[error("failed to initialise templates from explcit dir: {0:#}")]
72    Templates(#[source] AE),
73
74    #[error("internal error during startup: {0}")]
75    Internal(#[from] InternalError),
76}
77
78#[derive(Error, Debug)]
79pub enum NotForUsReason {
80    #[error("unexpected/incomplete JSON payload: {0}")]
81    DeserFailed(String),
82
83    #[error("webhook event is tag being deleted")]
84    TagIsBeingDeleted,
85
86    #[error("tag name has unexpected syntax (not DEP-14)")]
87    TagNameUnexpectedSyntax,
88
89    #[error("tag name doesn't start with our DEP-14 distro name")]
90    TagNameNotOurDistro,
91
92    #[error("tag message has only summary/title, no body")]
93    TagWithoutMessageBody,
94
95    #[error("no [dgit please-upload] instruction")]
96    NoPleaseUpload,
97
98    #[error("missing [dgit source= ] information (old git-debpush?)")]
99    MissingSource,
100
101    #[error("missing [dgit version= ] information (old git-debpush?)")]
102    MissingVersion,
103
104    #[error("no [dgit distro=...] for the distro we support")]
105    MetaNotOurDistro,
106
107    #[error("bad metadata item: {item:?}: {error}")]
108    BadMetadataItem { item: String, error: MetadataItemError, },
109
110    #[error("source package not mentioned in passlist")]
111    PackageNotPasslisted,
112
113    #[error("tag is too old ({age} > {max})")]
114    TagTooOld {
115        age: humantime::Duration,
116        max: humantime::Duration,
117    },
118
119    #[error("tag is too new by {skew} (> {max})")]
120    TagTooNew {
121        skew: humantime::Duration,
122        max: humantime::Duration,
123    },
124}
125
126#[derive(Error, Debug)]
127pub enum WebError {
128    #[error("misconfigured (or malfunctioning) web hook: {0:#}")]
129    MisconfiguredWebhook(AE),
130    #[error("{0}")]
131    InternalError(#[from] InternalError),
132    #[error("tag is not for us: {0}")]
133    NotForUs(#[from] NotForUsReason),
134    #[error("page not found at this URL: {0:#}")]
135    PageNotFoundHere(AE),
136    #[error("{0}")]
137    DisallowedClient(#[from] dns::DisallowedClient),
138    #[error("service throttled: {0}")]
139    Throttled(String),
140}
141
142#[derive(Error, Debug)]
143pub enum OracleTaskError {
144    #[error("oracle disconnected")]
145    Disconnected,
146
147    #[error("I/O error on oracle connection")]
148    Io(Arc<io::Error>),
149
150    #[error("malformed message received: {0}")]
151    BadMessage(#[from] o2m_support::BadMessage),
152
153    #[error("peer reported protocol violation: {0}")]
154    PeerReportedProtocolViolation(#[from] o2m_messages::ProtocolViolation),
155
156    #[error("peer requested protocol version {}; not supported", .0.version)]
157    UnsupportedVersion(o2m_messages::VersionRequest),
158
159    #[error("maximum line length (`limits.o2m_line`) exceeded")]
160    MaxLineLengthExceeded,
161
162    #[error("{0}")]
163    InternalError(#[from] InternalError),
164
165    #[error("shutting down")]
166    Shutdown(ShuttingDown),
167
168    #[error("restarting old worker")]
169    RestartingWorker,
170}
171
172
173#[derive(Error, Debug)]
174#[error("mismatch (possible race): {what}: earlier={earlier:?} now={now:?}")]
175pub struct MismatchError {
176    what: String,
177    earlier: String,
178    now: String,
179}
180
181//==================== impls ====================
182
183impl MismatchError {
184    pub fn check<'t, T: Display + Eq>(
185        what: impl Display,
186        earlier: &'t T,
187        now: &'t T,
188    ) -> Result<&'t T, MismatchError> {
189        if earlier == now {
190            Ok(earlier)
191        } else {
192            Err(MismatchError {
193                what: what.to_string(),
194                earlier: earlier.to_string(),
195                now: now.to_string(),
196            })
197        }
198    }
199}
200
201impl WebError {
202    fn http_status(&self) -> rocket::http::Status {
203        use rocket::http::Status as S;
204        match self {
205            WE::MisconfiguredWebhook(_) => S::BadRequest,
206            WE::InternalError(_) => S::InternalServerError,
207            WE::PageNotFoundHere(_) => S::NotFound,
208            WE::NotForUs { .. } => S::Ok,
209            WE::Throttled(..) => S::ServiceUnavailable,
210            WE::DisallowedClient(dc) => dc.http_status(),
211        }
212    }
213}
214
215impl<'r, 'o: 'r> rocket::response::Responder<'r, 'o> for WebError {
216    fn respond_to(
217        self,
218        _req: &'r rocket::request::Request<'_>,
219    ) -> rocket::response::Result<'o> {
220        let msg = format!("error: {self}");
221        Ok(rocket::response::Response::build()
222           .status(self.http_status())
223           .sized_body(None, Cursor::new(msg))
224           .header(rocket::http::ContentType::Text)
225           .finalize())
226    }
227}
228
229//==================== internal error ===================
230
231/// Internal error
232///
233/// Invariant: if one of these exists, it has been logged
234/// already, and shutdown has been triggered.
235#[derive(Error, Debug, Clone)]
236// Display impl doesn't print the Backtrace; we do that on creation, only.
237#[error("internal error: {:#}", self.0.ae)]
238pub struct InternalError(Arc<InternalErrorPayload>);
239
240#[derive(derive_more::Display, Debug)]
241#[display("{ae:#}\n{backtrace}")]
242struct InternalErrorPayload {
243    ae: anyhow::Error,
244    backtrace: Backtrace,
245}
246
247macro_rules! internal { { $fmt:literal $($rest:tt)* } => {
248    IE::new(anyhow!($fmt $($rest)*))
249} }
250
251impl InternalError {
252    #[track_caller]
253    pub fn new(ae: AE) -> InternalError {
254        IE::new_inner(ae, Backtrace::force_capture())
255    }
256
257    pub fn new_without_backtrace(ae: AE) -> InternalError {
258        IE::new_inner(ae, Backtrace::disabled())
259    }
260
261    /// Make an `InternalError` that doesn't cause shutdown
262    ///
263    /// Nor does it produce a backtrace
264    pub fn new_quiet(ae: AE) -> InternalError {
265        let backtrace = Backtrace::disabled();
266        error!("internal error - but carrying on! {ae:#}");
267        let pl = InternalErrorPayload { ae, backtrace };
268        InternalError(pl.into())
269    }
270
271    /// Dispose of an `InternalError` that can't be reported
272    ///
273    /// The error will still be logged, and will still cause shutdown.
274    pub fn note_only(self) {
275        // Logging and shutdown are done in construction
276    }
277
278    #[track_caller]
279    fn new_inner(ae: AE, backtrace: Backtrace) -> InternalError {
280        let pl = InternalErrorPayload { ae, backtrace };
281        let ie = InternalError(pl.into());
282
283        #[cfg(test)]
284        test::internal_error_hook(&ie);
285
286        globals().state.send_modify(|state| {
287            state.note_internal_error_inner(ie.clone())
288        });
289        ie
290    }
291
292    #[cfg(test)]
293    pub fn display_with_backtrace(&self) -> &impl Display { &self.0 }
294}
295
296impl global::State {
297    fn note_internal_error_inner(&mut self, ie: InternalError) {
298        let pl = &ie.0;
299        let store = match &mut self.shutdown_reason {
300            reason @ None => {
301                error!("internal error, will shut down! {pl}");
302                Some(reason)
303            }
304            reason @ Some(Ok(ShuttingDown)) => {
305                error!("internal error during shutdown! {pl}");
306                Some(reason)
307            }
308            _already @ Some(Err(_)) => {
309                info!("additional internal error! {pl}");
310                None
311            }
312        };
313        if let Some(store) = store {
314            *store = Some(Err(ie));
315        }
316    }
317}
318
319pub trait IntoInternal: Sized {
320    #[track_caller]
321    // TODO this API forces a lot of consing of error msgs on success paths
322    fn into_internal(self, what: impl Display)
323                     -> InternalError;
324}
325impl<E> IntoInternal for E where anyhow::Error: From<E> {
326    #[track_caller]
327    fn into_internal(self, what: impl Display)
328                     -> InternalError {
329        let what = what.to_string();
330        IE::new(anyhow::Error::from(self).context(what))
331    }
332}
333
334#[ext(IntoInternalResult)]
335pub impl<T, E: IntoInternal> Result<T, E> {
336    #[track_caller]
337    fn into_internal(self, what: impl Display)
338                         -> Result<T, InternalError> {
339        self.map_err(move |e| e.into_internal(what))
340    }
341}
342#[ext(IntoInternalOption)]
343pub impl<T> Option<T> {
344    #[track_caller]
345    fn ok_or_internal(self, what: impl Display)
346                         -> Result<T, InternalError> {
347        self.ok_or_else(|| IE::new(anyhow!(what.to_string())))
348    }
349}
350
351impl InternalError {
352    // Everything should be implemented, so this shouldn't be called in prod.
353    // We retain it in case we need it again.
354    // Marking it #[cfg(test)] means it will get compiled.
355    // When adding a real call site, add a blocking TODO of some kind.
356    #[cfg(test)]
357    pub fn nyi(what: &str) -> InternalError {
358        // Construct *without* going through IE::new.
359        // So this doesn't cause
360        //  (a) shutdown
361        //  (b) test failures
362        let ae = anyhow!("not yet implemented")
363                .context(what.to_string());
364        IE::new_quiet(ae)
365    }
366}