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
//! Whether GitHub can be reached. A request that can't connect (no
//! network, the server down, DNS failing) marks GitHub unreachable; any
//! answer at all marks it reachable again. While it's unreachable,
//! requests fail at once ([`ApiError::Offline`]) instead of waiting out
//! their timeouts, so nothing on screen loads forever, and the cache
//! answers instead. Only [`GitHub::probe`] asks meanwhile, to find out
//! when it's back.
//!
//! An [`ApiError::Offline`] always means the request never left: nothing
//! was sent, so a write that failed so can be queued and sent later
//! without being sent twice.
use std::sync::Arc;
use tokio::sync::watch;
use crate::api::{ApiError, GitHub};
/// Since when GitHub has been unreachable (Unix seconds), if it is.
/// Cheap to clone; clones share one state, and anyone may watch it.
#[derive(Debug, Clone)]
pub struct Reach(Arc<watch::Sender<Option<u64>>>);
impl Default for Reach {
fn default() -> Self {
Self(Arc::new(watch::Sender::new(None)))
}
}
impl Reach {
/// Since when GitHub has been unreachable, if it is.
pub fn offline_since(&self) -> Option<u64> {
*self.0.borrow()
}
/// A request couldn't connect.
pub(crate) fn lost(&self) {
self.0.send_if_modified(|since| {
let was = since.is_some();
since.get_or_insert_with(|| crate::store::now().max(1));
!was
});
}
/// GitHub answered.
pub(crate) fn found(&self) {
self.0.send_if_modified(|since| since.take().is_some());
}
/// Follows the changes from now on.
pub fn watch(&self) -> watch::Receiver<Option<u64>> {
self.0.subscribe()
}
}
impl GitHub {
/// Since when GitHub has been unreachable, if it is.
pub fn offline_since(&self) -> Option<u64> {
self.reach().offline_since()
}
/// Asks GitHub whether it's there: any answer at all (even a refusal)
/// is. Unlike other requests, it's sent while GitHub is unreachable.
pub async fn probe(&self) -> bool {
match self.probe_once().await {
Ok(()) => true,
Err(err) => {
tracing::debug!(%err, "GitHub still can't be reached");
false
}
}
}
}
/// What a request that never left says: why, at its root ("Connection
/// refused"), not the request it was.
pub(crate) fn offline(err: &(dyn std::error::Error + 'static)) -> ApiError {
let mut root = err;
while let Some(source) = root.source() {
root = source;
}
ApiError::Offline(root.to_string())
}