nap_core/repository_api/
fallback.rs1use anyhow::{Context, Result};
9use tracing::{info, warn};
10
11use super::RepositoryApi;
12use crate::provider::{ProviderFactory, ProviderType};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum FallbackStrategy {
17 None,
19 Auto,
21 Prompt,
23}
24
25#[derive(Debug, Clone)]
27pub enum FallbackResult {
28 NotNeeded,
30 Success {
32 original_provider: ProviderType,
33 fallback_provider: ProviderType,
34 },
35 Failed {
37 original_provider: ProviderType,
38 error: String,
39 },
40 Declined { original_provider: ProviderType },
42}
43
44pub struct FallbackHandler {
46 strategy: FallbackStrategy,
47 nap_home: std::path::PathBuf,
48}
49
50impl FallbackHandler {
51 pub fn new(nap_home: &std::path::Path) -> Self {
53 Self {
54 strategy: FallbackStrategy::Prompt,
55 nap_home: nap_home.to_path_buf(),
56 }
57 }
58
59 pub fn with_strategy(mut self, strategy: FallbackStrategy) -> Self {
61 self.strategy = strategy;
62 self
63 }
64
65 pub async fn handle_provider_failure(
67 &self,
68 repository_api: &mut RepositoryApi,
69 original_provider: ProviderType,
70 error: &str,
71 ) -> Result<FallbackResult> {
72 warn!(
73 "Provider failure detected: {} - {}",
74 original_provider.as_str(),
75 error
76 );
77
78 if !matches!(
80 original_provider,
81 ProviderType::PortalsCloud | ProviderType::Remote
82 ) {
83 return Ok(FallbackResult::Failed {
84 original_provider,
85 error: format!(
86 "Cannot fallback from {} provider",
87 original_provider.as_str()
88 ),
89 });
90 }
91
92 match self.strategy {
93 FallbackStrategy::None => Ok(FallbackResult::Failed {
94 original_provider,
95 error: "Fallback disabled".to_string(),
96 }),
97 FallbackStrategy::Auto => {
98 self.perform_fallback(repository_api, original_provider)
99 .await
100 }
101 FallbackStrategy::Prompt => {
102 info!("Prompting user for fallback to local provider");
105 self.perform_fallback(repository_api, original_provider)
106 .await
107 }
108 }
109 }
110
111 async fn perform_fallback(
113 &self,
114 repository_api: &mut RepositoryApi,
115 original_provider: ProviderType,
116 ) -> Result<FallbackResult> {
117 info!("Attempting fallback to local provider");
118
119 let factory = ProviderFactory::new(&self.nap_home);
120 let local_provider = factory
121 .create_provider(ProviderType::Local)
122 .context("Failed to create local provider for fallback")?;
123
124 local_provider
126 .initialize()
127 .await
128 .context("Failed to initialize local provider during fallback")?;
129
130 local_provider
132 .ensure_ready()
133 .await
134 .context("Failed to ensure local provider ready during fallback")?;
135
136 repository_api
138 .provider_manager_mut()
139 .set_active_provider(local_provider.clone());
140
141 repository_api
143 .provider_manager_mut()
144 .save_provider_config(local_provider.as_ref())?;
145
146 info!("Fallback to local provider successful");
147
148 Ok(FallbackResult::Success {
149 original_provider,
150 fallback_provider: ProviderType::Local,
151 })
152 }
153
154 pub fn should_offer_fallback(&self, error: &str) -> bool {
156 error.to_lowercase().contains("unavailable")
158 || error.to_lowercase().contains("timeout")
159 || error.to_lowercase().contains("connection")
160 || error.to_lowercase().contains("network")
161 }
162
163 pub fn fallback_message(&self, original_provider: ProviderType) -> String {
165 match original_provider {
166 ProviderType::PortalsCloud => "Portals Cloud is currently unavailable.\n\
167 Start a local Lore server instead?\n\
168 Changes will remain local until synchronization.\n\
169 [Y/n]"
170 .to_string(),
171 ProviderType::Remote => "Remote Lore server is currently unavailable.\n\
172 Start a local Lore server instead?\n\
173 Changes will remain local until synchronization.\n\
174 [Y/n]"
175 .to_string(),
176 ProviderType::Local => "Local provider failed. No fallback available.".to_string(),
177 }
178 }
179}
180
181pub trait RepositoryApiFallback {
183 fn with_fallback<F, Fut, T>(
189 &mut self,
190 operation: F,
191 ) -> impl std::future::Future<Output = Result<T>> + Send
192 where
193 F: FnMut(&mut Self) -> Fut + Send,
194 Fut: std::future::Future<Output = Result<T>> + Send,
195 T: Send + 'static;
196
197 fn ensure_provider_with_fallback(
200 &mut self,
201 ) -> impl std::future::Future<Output = Result<bool>> + Send;
202}
203
204impl RepositoryApiFallback for RepositoryApi {
205 async fn with_fallback<F, Fut, T>(&mut self, mut operation: F) -> Result<T>
206 where
207 F: FnMut(&mut Self) -> Fut + Send,
208 Fut: std::future::Future<Output = Result<T>> + Send,
209 T: Send + 'static,
210 {
211 let result = operation(self).await;
213
214 if let Err(e) = &result {
215 if let Some(provider) = self.active_provider() {
217 let handler = FallbackHandler::new(&self.nap_home);
218 let provider_type = provider.provider_type();
219
220 if handler.should_offer_fallback(&e.to_string()) {
221 let fallback_result = handler
222 .handle_provider_failure(self, provider_type, &e.to_string())
223 .await?;
224
225 match fallback_result {
226 FallbackResult::Success { .. } => {
227 operation(self).await
229 }
230 FallbackResult::Declined { .. } => result,
231 FallbackResult::Failed { error, .. } => Err(anyhow::anyhow!(error)),
232 FallbackResult::NotNeeded => result,
233 }
234 } else {
235 result
236 }
237 } else {
238 result
239 }
240 } else {
241 result
242 }
243 }
244
245 async fn ensure_provider_with_fallback(&mut self) -> Result<bool> {
246 let handler = FallbackHandler::new(&self.nap_home);
247
248 if let Some(provider) = self.active_provider() {
249 let provider_type = provider.provider_type();
250
251 let is_healthy = provider.health_check().await.unwrap_or(false);
253
254 if is_healthy {
255 Ok(true)
256 } else {
257 let fallback_result = handler
259 .handle_provider_failure(self, provider_type, "Provider health check failed")
260 .await
261 .map_err(|e| anyhow::anyhow!(e))?;
262
263 match fallback_result {
264 FallbackResult::Success { .. } => Ok(true),
265 FallbackResult::Declined { .. } => Ok(false),
266 FallbackResult::Failed { error, .. } => Err(anyhow::anyhow!(error)),
267 FallbackResult::NotNeeded => Ok(true),
268 }
269 }
270 } else {
271 Ok(false)
272 }
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 use tempfile::TempDir;
280
281 #[test]
282 fn test_fallback_handler_creation() {
283 let temp_dir = TempDir::new().unwrap();
284 let handler = FallbackHandler::new(temp_dir.path());
285 assert_eq!(handler.strategy, FallbackStrategy::Prompt);
286 }
287
288 #[test]
289 fn test_fallback_strategy() {
290 let temp_dir = TempDir::new().unwrap();
291 let handler = FallbackHandler::new(temp_dir.path()).with_strategy(FallbackStrategy::Auto);
292 assert_eq!(handler.strategy, FallbackStrategy::Auto);
293 }
294
295 #[test]
296 fn test_should_offer_fallback() {
297 let temp_dir = TempDir::new().unwrap();
298 let handler = FallbackHandler::new(temp_dir.path());
299
300 assert!(handler.should_offer_fallback("Service unavailable"));
301 assert!(handler.should_offer_fallback("Connection timeout"));
302 assert!(handler.should_offer_fallback("Network error"));
303 assert!(!handler.should_offer_fallback("Permission denied"));
304 }
305
306 #[test]
307 fn test_fallback_message() {
308 let temp_dir = TempDir::new().unwrap();
309 let handler = FallbackHandler::new(temp_dir.path());
310
311 let message = handler.fallback_message(ProviderType::PortalsCloud);
312 assert!(message.contains("Portals Cloud is currently unavailable"));
313 assert!(message.contains("Start a local Lore server instead"));
314
315 let message = handler.fallback_message(ProviderType::Remote);
316 assert!(message.contains("Remote Lore server is currently unavailable"));
317
318 let message = handler.fallback_message(ProviderType::Local);
319 assert!(message.contains("No fallback available"));
320 }
321}