gpui_updater_core/source/
fallback.rs1use std::fmt;
4
5use super::UpdateSource;
6use crate::error::Result;
7use crate::release::Release;
8
9pub struct FallbackSource {
29 primary: Box<dyn UpdateSource>,
30 fallbacks: Vec<Box<dyn UpdateSource>>,
31}
32
33impl FallbackSource {
34 #[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 #[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 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}