Skip to main content

gpui_updater_core/source/
fallback.rs

1//! [`FallbackSource`]: ask several sources in order.
2
3use std::fmt;
4
5use super::UpdateSource;
6use crate::error::Result;
7use crate::release::Release;
8
9/// Asks several sources in order and returns the release from the first one
10/// that answers: a mirror or a second host for when the primary is down.
11///
12/// The first source that answers wins, even when a later one knows a newer
13/// release; the sources are not compared. When every source fails, the error
14/// is the primary's, and every failure is logged at `warn`.
15///
16/// Each source checks what it fetches with its own settings, such as a
17/// manifest's signing keys. The engine checks the artifact with the same
18/// [`EngineConfig`](crate::EngineConfig) whichever source named it.
19///
20/// ```
21/// use gpui_updater_core::{FallbackSource, GitHubSource, StaticManifestSource};
22///
23/// let source = FallbackSource::new(StaticManifestSource::new(
24///     "https://updates.example.com/latest.json",
25/// ))
26/// .or(GitHubSource::new("owner", "repo"));
27/// ```
28pub struct FallbackSource {
29    primary: Box<dyn UpdateSource>,
30    fallbacks: Vec<Box<dyn UpdateSource>>,
31}
32
33impl FallbackSource {
34    /// Ask `primary` first.
35    #[must_use]
36    pub fn new(primary: impl UpdateSource) -> Self {
37        Self {
38            primary: Box::new(primary),
39            fallbacks: Vec::new(),
40        }
41    }
42
43    /// Ask `next` when every source before it has failed.
44    #[must_use]
45    pub fn or(mut self, next: impl UpdateSource) -> Self {
46        self.fallbacks.push(Box::new(next));
47        self
48    }
49}
50
51impl fmt::Debug for FallbackSource {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        f.debug_struct("FallbackSource")
54            .field("sources", &(1 + self.fallbacks.len()))
55            .finish_non_exhaustive()
56    }
57}
58
59impl UpdateSource for FallbackSource {
60    fn fetch_latest(&self) -> Result<Release> {
61        let primary_error = match self.primary.fetch_latest() {
62            Ok(release) => return Ok(release),
63            Err(error) => error,
64        };
65        tracing::warn!(error = %primary_error, "the primary update source failed");
66        for (index, source) in self.fallbacks.iter().enumerate() {
67            match source.fetch_latest() {
68                Ok(release) => {
69                    tracing::info!(fallback = index + 1, "using a fallback update source");
70                    return Ok(release);
71                }
72                Err(error) => {
73                    tracing::warn!(fallback = index + 1, %error, "a fallback update source failed");
74                }
75            }
76        }
77        Err(primary_error)
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use std::sync::Arc;
84    use std::sync::atomic::{AtomicUsize, Ordering};
85
86    use super::*;
87    use crate::error::Error;
88    use crate::release::Asset;
89    use semver::Version;
90
91    /// Returns `release(major)`, or fails with `error`, counting its calls.
92    struct Fake {
93        answer: std::result::Result<u64, &'static str>,
94        calls: Arc<AtomicUsize>,
95    }
96
97    fn fake(answer: std::result::Result<u64, &'static str>) -> (Fake, Arc<AtomicUsize>) {
98        let calls = Arc::new(AtomicUsize::new(0));
99        let source = Fake {
100            answer,
101            calls: calls.clone(),
102        };
103        (source, calls)
104    }
105
106    impl UpdateSource for Fake {
107        fn fetch_latest(&self) -> Result<Release> {
108            self.calls.fetch_add(1, Ordering::SeqCst);
109            match self.answer {
110                Ok(major) => Ok(Release::new(
111                    Version::new(major, 0, 0),
112                    Asset::new("app.dmg", "https://example.com/app.dmg", 1),
113                )),
114                Err(message) => Err(Error::Http(message.to_string())),
115            }
116        }
117    }
118
119    #[test]
120    fn the_primary_wins_when_it_answers() {
121        let (primary, _) = fake(Ok(1));
122        let (mirror, mirror_calls) = fake(Ok(2));
123        let release = FallbackSource::new(primary)
124            .or(mirror)
125            .fetch_latest()
126            .unwrap();
127        assert_eq!(release.version, Version::new(1, 0, 0));
128        assert_eq!(mirror_calls.load(Ordering::SeqCst), 0);
129    }
130
131    #[test]
132    fn falls_back_in_order_and_stops_at_the_first_answer() {
133        let (primary, _) = fake(Err("primary down"));
134        let (second, _) = fake(Err("second down"));
135        let (third, _) = fake(Ok(3));
136        let (fourth, fourth_calls) = fake(Ok(4));
137        let release = FallbackSource::new(primary)
138            .or(second)
139            .or(third)
140            .or(fourth)
141            .fetch_latest()
142            .unwrap();
143        assert_eq!(release.version, Version::new(3, 0, 0));
144        assert_eq!(fourth_calls.load(Ordering::SeqCst), 0);
145    }
146
147    #[test]
148    fn returns_the_primary_error_when_every_source_fails() {
149        let (primary, _) = fake(Err("primary down"));
150        let (mirror, mirror_calls) = fake(Err("mirror down"));
151        let source = FallbackSource::new(primary).or(mirror);
152        let err = source.fetch_latest().unwrap_err();
153        assert_eq!(
154            err.to_string(),
155            Error::Http("primary down".into()).to_string()
156        );
157        assert_eq!(mirror_calls.load(Ordering::SeqCst), 1);
158        assert_eq!(format!("{source:?}"), "FallbackSource { sources: 2, .. }");
159    }
160}