tag2upload_service_manager/
error.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350

use crate::prelude::*;
use std::backtrace::Backtrace;

#[derive(Error, Debug)]
pub enum ProcessingError {
    #[error("problem at forge: {0:#}")]
    Forge(anyhow::Error),

    #[error("local problem {0:#}")]
    Local(anyhow::Error),

    #[error("{0}")]
    Mismatch(#[from] MismatchError),

    #[error("{0}")]
    Internal(#[from] InternalError),
}

/// Token indicating that we are OK to shut this task down
///
/// Returned by the closure passed to [`Globals::spawn-task`].
///
/// Not an error.
pub struct TaskWorkComplete {}

/// Not an `Error`
#[derive(Debug, From)]
pub enum QuitTask {
    Crashed(InternalError),
    Shutdown(ShuttingDown),
}

pub type TaskResult = Result<TaskWorkComplete, QuitTask>;

#[derive(Error, Debug)]
pub enum StartupError {
    #[error("failed to parse/deserialise t2u configuration: {0}")]
    ParseConfig(figment::Error),

    #[error("invalid configuration")]
    InvalidConfig,

    #[error("problem with temp directory: {0:#}")]
    TempDir(AE),

    #[error("failed to initialise http client: {0}")]
    Reqwest(#[from] reqwest::Error),

    #[error("failed to initialise DNS resolver: {0}")]
    Resolver(#[from] hickory_resolver::error::ResolveError),

    #[error("failed to initialise Rocket http server: {0}")]
    Rocket(#[from] rocket::Error),

    #[error("failed to initialise worker listener(s): {0:#}")]
    WorkerListener(AE),

    #[error("failed to open database: {0}")]
    DbOpen(rusqlite::Error),

    #[error("failed to idempotently initialise db schema: {0}")]
    ExecuteSchema(rusqlite::Error),

    #[error("failed to access database during startup: {0}")]
    DbAccess(rusqlite::Error),

    #[error("failed to initialise logging: {0:#}")]
    Logging(AE),

    #[error("failed to initialise templates from explcit dir: {0:#}")]
    Templates(#[source] AE),

    #[error("internal error during startup: {0}")]
    Internal(#[from] InternalError),
}

#[derive(Error, Debug)]
pub enum NotForUsReason {
    #[error("webhook event is tag being deleted")]
    TagIsBeingDeleted,

    #[error("tag name has unexpected syntax (not DEP-14)")]
    TagNameUnexpectedSyntax,

    #[error("tag name doesn't start with our DEP-14 distro name")]
    TagNameNotOurDistro,

    #[error("tag message has only summary/title, no body")]
    TagWithoutMessageBody,

    #[error("no [dgit please-upload] instruction")]
    NoPleaseUpload,

    #[error("missing [dgit source= ] information (old git-debpush?)")]
    MissingSource,

    #[error("missing [dgit version= ] information (old git-debpush?)")]
    MissingVersion,

    #[error("no [dgit distro=...] for the distro we support")]
    MetaNotOurDistro,

    #[error("bad metadata item: {item:?}: {error}")]
    BadMetadataItem { item: String, error: MetadataItemError, },

    #[error("tag is too old ({age} > {max})")]
    TagTooOld {
        age: humantime::Duration,
        max: humantime::Duration,
    },

    #[error("tag is too new by {skew} (> {max})")]
    TagTooNew {
        skew: humantime::Duration,
        max: humantime::Duration,
    },
}

#[derive(Error, Debug)]
pub enum WebError {
    #[error("misconfigured web hook: {0:#}")]
    MisconfiguredWebhook(AE),
    #[error("web hook source is malfunctioning: {0:#}")]
    MalfunctioningWebhook(AE),
    #[error("network error: {0}")]
    NetworkError(AE),
    #[error("{0}")]
    InternalError(#[from] InternalError),
    #[error("tag is not for us: {0}")]
    NotForUs(#[from] NotForUsReason),
}

#[derive(Error, Debug)]
pub enum OracleTaskError {
    #[error("oracle disconnected")]
    Disconnected,

    #[error("I/O error on oracle connection")]
    Io(Arc<io::Error>),

    #[error("malformed message received: {0}")]
    BadMessage(#[from] o2m_support::BadMessage),

    #[error("peer reported protocol violation: {0}")]
    PeerReportedProtocolViolation(#[from] o2m_messages::ProtocolViolation),

    #[error("peer requested protocol version {}; not supported", .0.version)]
    UnsupportedVersion(o2m_messages::Version),

    #[error("maximum line length (`limits.o2m_line`) exceeded")]
    MaxLineLengthExceeded,

    #[error("{0}")]
    InternalError(#[from] InternalError),

    #[error("shutting down")]
    Shutdown(ShuttingDown),
}


#[derive(Error, Debug)]
#[error("mismatch (possible race): {what}: earlier={earlier:?} now={now:?}")]
pub struct MismatchError {
    what: String,
    earlier: String,
    now: String,
}

/// Internal error
///
/// Invariant: if one of these exists, it has been logged
/// already, and shutdown has been triggered.
#[derive(Error, Debug, Clone)]
// Display impl doesn't print the Backtrace; we do that on creation, only.
#[error("internal error: {:#}", self.0.ae)]
pub struct InternalError(Arc<InternalErrorPayload>);

#[derive(derive_more::Display, Debug)]
#[display(fmt = "{ae:#}\n{backtrace}")]
struct InternalErrorPayload {
    ae: anyhow::Error,
    backtrace: Backtrace,
}

macro_rules! internal { { $fmt:literal $($rest:tt)* } => {
    IE::new(anyhow!($fmt $($rest)*))
} }

impl InternalError {
    #[track_caller]
    pub fn new(ae: AE) -> InternalError {
        IE::new_inner(ae, Backtrace::force_capture())
    }

    pub fn new_without_backtrace(ae: AE) -> InternalError {
        IE::new_inner(ae, Backtrace::disabled())
    }

    /// Make an `InternalError` that doesn't cause shutdown
    ///
    /// Nor does it produce a backtrace
    pub fn new_quiet(ae: AE) -> InternalError {
        let backtrace = Backtrace::disabled();
        error!("internal error - NYI, carrying on! {ae:#}");
        let pl = InternalErrorPayload { ae, backtrace };
        InternalError(pl.into())
    }

    /// Dispose of an `InternalError` that can't be reported
    ///
    /// The error will still be logged, and will still cause shutdown.
    pub fn note_only(self) {
        // Logging and shutdown are done in construction
    }

    #[track_caller]
    fn new_inner(ae: AE, backtrace: Backtrace) -> InternalError {
        let pl = InternalErrorPayload { ae, backtrace };
        let ie = InternalError(pl.into());

        #[cfg(test)]
        test::internal_error_hook(&ie);

        globals().state.send_modify(|state| {
            state.note_internal_error_inner(ie.clone())
        });
        ie
    }

    #[cfg(test)]
    pub fn display_with_backtrace(&self) -> &impl Display { &self.0 }
}

impl global::State {
    fn note_internal_error_inner(&mut self, ie: InternalError) {
        let pl = &ie.0;
        let store = match &mut self.shutdown_reason {
            reason @ None => {
                error!("internal error, will shut down! {pl}");
                Some(reason)
            }
            reason @ Some(Ok(ShuttingDown)) => {
                error!("internal error during shutdown! {pl}");
                Some(reason)
            }
            _already @ Some(Err(_)) => {
                info!("additional internal error! {pl}");
                None
            }
        };
        if let Some(store) = store {
            *store = Some(Err(ie));
        }
    }
}

pub trait IntoInternal: Sized {
    #[track_caller]
    // TODO this API forces a lot of consing of error msgs on success paths
    fn into_internal(self, what: impl Display)
                     -> InternalError;
}
impl<E> IntoInternal for E where anyhow::Error: From<E> {
    #[track_caller]
    fn into_internal(self, what: impl Display)
                     -> InternalError {
        let what = what.to_string();
        IE::new(anyhow::Error::from(self).context(what))
    }
}

impl MismatchError {
    pub fn check<'t, T: Display + Eq>(
        what: impl Display,
        earlier: &'t T,
        now: &'t T,
    ) -> Result<&'t T, MismatchError> {
        if earlier == now {
            Ok(earlier)
        } else {
            Err(MismatchError {
                what: what.to_string(),
                earlier: earlier.to_string(),
                now: now.to_string(),
            })
        }
    }
}

#[ext(IntoInternalResult)]
pub impl<T, E: IntoInternal> Result<T, E> {
    #[track_caller]
    fn into_internal(self, what: impl Display)
                         -> Result<T, InternalError> {
        self.map_err(move |e| e.into_internal(what))
    }
}
#[ext(IntoInternalOption)]
pub impl<T> Option<T> {
    #[track_caller]
    fn ok_or_internal(self, what: impl Display)
                         -> Result<T, InternalError> {
        self.ok_or_else(|| IE::new(anyhow!(what.to_string())))
    }
}

impl InternalError {
    // Everything should be implemented, so this shouldn't be called in prod.
    // We retain it in case we need it again.
    // Marking it #[cfg(test)] means it will get compiled.
    // When adding a real call site, add a blocking TODO of some kind.
    #[cfg(test)]
    pub fn nyi(what: &str) -> InternalError {
        // Construct *without* going through IE::new.
        // So this doesn't cause
        //  (a) shutdown
        //  (b) test failures
        let ae = anyhow!("not yet implemented")
                .context(what.to_string());
        IE::new_quiet(ae)
    }
}

impl WebError {
    fn http_status(&self) -> rocket::http::Status {
        use rocket::http::Status as S;
        match self {
            WE::MisconfiguredWebhook(_) |
            WE::MalfunctioningWebhook(_) => S::BadRequest,
            WE::InternalError(_) => S::InternalServerError,
            WE::NetworkError(_) => S::ServiceUnavailable,
            WE::NotForUs(_) => S::Ok,
        }
    }
}

impl<'r, 'o: 'r> rocket::response::Responder<'r, 'o> for WebError {
    fn respond_to(
        self,
        _req: &'r rocket::request::Request<'_>,
    ) -> rocket::response::Result<'o> {
        let msg = format!("error: {self}");
        Ok(rocket::response::Response::build()
           .status(self.http_status())
           .sized_body(None, Cursor::new(msg))
           .header(rocket::http::ContentType::Text)
           .finalize())
    }
}