diff --git a/docs/cli/index.md b/docs/cli/index.md index 81f6222224..65d7b78f7c 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -94,7 +94,7 @@ Can also use `MISE_NO_CONFIG=1` - [`mise generate tool-stub [FLAGS] `](/cli/generate/tool-stub.md) - [`mise implode [--config] [-n --dry-run]`](/cli/implode.md) - [`mise install [FLAGS] [TOOL@VERSION]…`](/cli/install.md) -- [`mise install-into `](/cli/install-into.md) +- [`mise install-into [--retry ] `](/cli/install-into.md) - [`mise latest [-i --installed] `](/cli/latest.md) - [`mise link [-f --force] `](/cli/link.md) - [`mise lock [FLAGS] [TOOL]…`](/cli/lock.md) diff --git a/docs/cli/install-into.md b/docs/cli/install-into.md index 236a401273..b27b429c2c 100644 --- a/docs/cli/install-into.md +++ b/docs/cli/install-into.md @@ -1,6 +1,6 @@ # `mise install-into` -- **Usage**: `mise install-into ` +- **Usage**: `mise install-into [--retry ] ` - **Source code**: [`src/cli/install_into.rs`](https://github.com/jdx/mise/blob/main/src/cli/install_into.rs) Install a tool version to a specific path @@ -17,6 +17,12 @@ Tool to install e.g.: node@20 Path to install the tool into +## Flags + +### `--retry ` + +Retry installation if it fails due to transient errors, e.g. network issues + Examples: ``` diff --git a/docs/cli/install.md b/docs/cli/install.md index de5983db19..151c76dca2 100644 --- a/docs/cli/install.md +++ b/docs/cli/install.md @@ -45,6 +45,10 @@ Show installation output This argument will print plugin output such as download, configuration, and compilation output. +### `--retry ` + +Retry installation if it fails due to transient errors, e.g. network issues + Examples: ``` diff --git a/mise.usage.kdl b/mise.usage.kdl index f4e7489b6c..6994f16053 100644 --- a/mise.usage.kdl +++ b/mise.usage.kdl @@ -452,11 +452,17 @@ cmd install help="Install a tool version" { flag "-v --verbose" help="Show installation output" var=#true count=#true { long_help "Show installation output\n\nThis argument will print plugin output such as download, configuration, and compilation output." } + flag --retry help="Retry installation if it fails due to transient errors, e.g. network issues" { + arg + } arg "[TOOL@VERSION]…" help="Tool(s) to install e.g.: node@20" required=#false var=#true } cmd install-into help="Install a tool version to a specific path" { long_help "Install a tool version to a specific path\n\nUsed for building a tool to a directory for use outside of mise" after_long_help "Examples:\n\n # install node@20.0.0 into ./mynode\n $ mise install-into node@20.0.0 ./mynode && ./mynode/bin/node -v\n 20.0.0\n" + flag --retry help="Retry installation if it fails due to transient errors, e.g. network issues" { + arg + } arg help="Tool to install e.g.: node@20" arg help="Path to install the tool into" } diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 45bc07e674..16a28d0617 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -6,11 +6,13 @@ use std::hash::Hash; use std::ops::Deref; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; +use std::time::Duration; use tokio::sync::Mutex as TokioMutex; use crate::cli::args::{BackendArg, ToolVersionType}; use crate::cmd::CmdLineRunner; use crate::config::{Config, Settings}; +use crate::errors::Error; use crate::file::{display_path, remove_all, remove_all_with_warning}; use crate::install_context::InstallContext; use crate::lockfile::PlatformInfo; @@ -502,13 +504,60 @@ pub trait Backend: Debug + Send + Sync { let _lock = lock_file::get(&tv.install_path(), ctx.force)?; self.create_install_dirs(&tv)?; - let old_tv = tv.clone(); - let tv = match self.install_version_(&ctx, tv).await { - Ok(tv) => tv, - Err(e) => { - self.cleanup_install_dirs_on_error(&old_tv); - // Pass through the error - it will be wrapped at a higher level - return Err(e); + let mut current_retry = 0; + let tv = loop { + match self.install_version_(&ctx, tv.clone()).await { + Ok(tv) => break tv, + Err(e) => { + self.cleanup_install_dirs_on_error(&tv); + + trait IsRetryable { + fn is_retryable(&self) -> bool; + } + + impl IsRetryable for eyre::Report { + fn is_retryable(&self) -> bool { + if let Some(reqwest_error) = self.downcast_ref::() { + return reqwest_error.is_request() + && (reqwest_error.is_connect() || reqwest_error.is_timeout()); + } + + false + } + } + + if current_retry < ctx.retry && e.is_retryable() { + const MAX_BACKOFF_TIME: u64 = 60; + + let backoff_time = 2u64.pow(current_retry.into()).min(MAX_BACKOFF_TIME); + current_retry += 1; + ctx.pr.println(format!( + "{}", + Error::InstallFailed { + successful_installations: vec![], + failed_installations: vec![(tv.request.clone(), e)], + } + )); + ctx.pr.println(format!( + "Retrying installation … ({current_retry}/{})", + ctx.retry + )); + + for i in 0..backoff_time { + ctx.pr.set_message(format!( + "install failed (retrying in {} seconds …)", + backoff_time - i + )); + tokio::time::sleep(Duration::from_secs(1)).await; + } + ctx.pr.set_message("install".into()); + + continue; + } + + // Pass through the error - it will be wrapped at a higher level + return Err(e); + } } }; diff --git a/src/cli/install.rs b/src/cli/install.rs index 642726e53b..97be0a1323 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -48,6 +48,10 @@ pub struct Install { /// This argument will print plugin output such as download, configuration, and compilation output. #[clap(long, short, action = clap::ArgAction::Count)] verbose: u8, + + /// Retry installation if it fails due to transient errors, e.g. network issues. + #[clap(long, default_value_t = 0)] + retry: u8, } impl Install { @@ -117,6 +121,7 @@ impl Install { latest_versions: true, }, dry_run: self.dry_run, + retry: self.retry, ..Default::default() } } diff --git a/src/cli/install_into.rs b/src/cli/install_into.rs index 1b00c9bf14..cec590ac85 100644 --- a/src/cli/install_into.rs +++ b/src/cli/install_into.rs @@ -21,6 +21,10 @@ pub struct InstallInto { /// Path to install the tool into #[clap(value_hint = ValueHint::DirPath)] path: PathBuf, + + /// Retry installation if it fails due to transient errors, e.g. network issues. + #[clap(long, default_value_t = 0)] + retry: u8, } impl InstallInto { @@ -48,6 +52,7 @@ impl InstallInto { pr: mpr.add(&tv.style()), force: true, dry_run: false, + retry: self.retry, }; tv.install_path = Some(self.path.clone()); backend.install_version(install_ctx, tv).await?; diff --git a/src/install_context.rs b/src/install_context.rs index c770801233..c86e482080 100644 --- a/src/install_context.rs +++ b/src/install_context.rs @@ -9,4 +9,5 @@ pub struct InstallContext { pub pr: Box, pub force: bool, pub dry_run: bool, + pub retry: u8, } diff --git a/src/toolset/mod.rs b/src/toolset/mod.rs index 6ce567c6a6..2a78078dd2 100644 --- a/src/toolset/mod.rs +++ b/src/toolset/mod.rs @@ -62,6 +62,7 @@ pub struct InstallOptions { pub auto_install_disable_tools: Option>, pub resolve_options: ResolveOptions, pub dry_run: bool, + pub retry: u8, } impl Default for InstallOptions { @@ -75,6 +76,7 @@ impl Default for InstallOptions { auto_install_disable_tools: Settings::get().auto_install_disable_tools.clone(), resolve_options: Default::default(), dry_run: false, + retry: 0, } } } @@ -454,6 +456,7 @@ impl Toolset { pr: mpr.add_with_options(&tv.style(), opts.dry_run), force: opts.force, dry_run: opts.dry_run, + retry: opts.retry, }; // Avoid wrapping the backend error here so the error location // points to the backend implementation (more helpful for debugging). diff --git a/xtasks/fig/src/mise.ts b/xtasks/fig/src/mise.ts index 50bf90fdf0..71281a4944 100644 --- a/xtasks/fig/src/mise.ts +++ b/xtasks/fig/src/mise.ts @@ -1374,6 +1374,15 @@ const completionSpec: Fig.Spec = { description: "Show installation output", isRepeatable: true, }, + { + name: "--retry", + description: + "Retry installation if it fails due to transient errors, e.g. network issues", + isRepeatable: false, + args: { + name: "retry", + }, + }, ], args: { name: "tool@version", @@ -1387,6 +1396,17 @@ const completionSpec: Fig.Spec = { { name: "install-into", description: "Install a tool version to a specific path", + options: [ + { + name: "--retry", + description: + "Retry installation if it fails due to transient errors, e.g. network issues", + isRepeatable: false, + args: { + name: "retry", + }, + }, + ], args: [ { name: "tool@version",