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