Conversation
Add retry logic with exponential backoff to CmdLineRunner::spawn() to handle ETXTBSY errors that occur when executing a binary immediately after extraction. This is a race condition on Linux where the file descriptor from the write/extract may not be fully closed before execve. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Summary of ChangesHello @jdx, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a robust mechanism to mitigate flaky CI failures observed on Linux systems. By implementing a retry strategy with exponential backoff for process spawning operations, it addresses a specific race condition where newly extracted or written binaries might not be immediately executable due to file descriptor contention, thereby improving the reliability of command execution. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Pull request overview
This PR fixes a race condition in CI where spawning processes fails with ETXTBSY errors on Linux when executing binaries immediately after extraction. The solution adds retry logic with exponential backoff to handle cases where file descriptors haven't fully closed yet.
Changes:
- Added
spawn_with_etxtbsy_retry()method with exponential backoff (50ms, 100ms, 200ms) to retry spawn operations up to 3 times - Implemented platform-specific
is_etxtbsy()helper usingnix::errno::Errnoon Unix systems - Applied retry logic to both normal and raw execution paths in
CmdLineRunner
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| attempt += 1; | ||
| trace!("retrying spawn after ETXTBSY (attempt {}/3)", attempt); | ||
| // Exponential backoff: 50ms, 100ms, 200ms | ||
| std::thread::sleep(std::time::Duration::from_millis(50 * (1 << (attempt - 1)))); |
There was a problem hiding this comment.
The magic number 50 for the base backoff delay should be extracted as a named constant (e.g., const ETXTBSY_BASE_DELAY_MS: u64 = 50;) to improve maintainability and make the backoff timing more visible.
| let mut attempt = 0; | ||
| loop { | ||
| match self.cmd.spawn() { | ||
| Ok(child) => return Ok(child), | ||
| Err(err) if Self::is_etxtbsy(&err) && attempt < 3 => { | ||
| attempt += 1; | ||
| trace!("retrying spawn after ETXTBSY (attempt {}/3)", attempt); |
There was a problem hiding this comment.
The magic number 3 for maximum retry attempts should be extracted as a named constant (e.g., const MAX_ETXTBSY_RETRIES: u32 = 3;) to improve maintainability and make the retry policy more explicit.
| let mut attempt = 0; | |
| loop { | |
| match self.cmd.spawn() { | |
| Ok(child) => return Ok(child), | |
| Err(err) if Self::is_etxtbsy(&err) && attempt < 3 => { | |
| attempt += 1; | |
| trace!("retrying spawn after ETXTBSY (attempt {}/3)", attempt); | |
| const MAX_ETXTBSY_RETRIES: i32 = 3; | |
| let mut attempt = 0; | |
| loop { | |
| match self.cmd.spawn() { | |
| Ok(child) => return Ok(child), | |
| Err(err) if Self::is_etxtbsy(&err) && attempt < MAX_ETXTBSY_RETRIES => { | |
| attempt += 1; | |
| trace!( | |
| "retrying spawn after ETXTBSY (attempt {}/{})", | |
| attempt, | |
| MAX_ETXTBSY_RETRIES | |
| ); |
There was a problem hiding this comment.
Code Review
This pull request introduces a retry mechanism for process spawning to handle ETXTBSY errors, which can occur in CI environments. The implementation with exponential backoff is solid and correctly applied to both execution paths. I've suggested a minor improvement to enhance code clarity and maintainability by replacing magic numbers with named constants.
| fn spawn_with_etxtbsy_retry(&mut self) -> std::io::Result<std::process::Child> { | ||
| let mut attempt = 0; | ||
| loop { | ||
| match self.cmd.spawn() { | ||
| Ok(child) => return Ok(child), | ||
| Err(err) if Self::is_etxtbsy(&err) && attempt < 3 => { | ||
| attempt += 1; | ||
| trace!("retrying spawn after ETXTBSY (attempt {}/3)", attempt); | ||
| // Exponential backoff: 50ms, 100ms, 200ms | ||
| std::thread::sleep(std::time::Duration::from_millis(50 * (1 << (attempt - 1)))); | ||
| } | ||
| Err(err) => return Err(err), | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The retry logic uses magic numbers for the maximum number of retries (3) and the initial backoff delay (50ms). It's a good practice to define these as named constants to improve readability and make them easier to modify in the future. I'd suggest defining them as const within the function since they are not used elsewhere.
fn spawn_with_etxtbsy_retry(&mut self) -> std::io::Result<std::process::Child> {
const MAX_RETRIES: u32 = 3;
const INITIAL_BACKOFF_MS: u64 = 50;
let mut attempt = 0;
loop {
match self.cmd.spawn() {
Ok(child) => return Ok(child),
Err(err) if Self::is_etxtbsy(&err) && attempt < MAX_RETRIES => {
attempt += 1;
trace!(
"retrying spawn after ETXTBSY (attempt {}/{})",
attempt,
MAX_RETRIES
);
// Exponential backoff: 50ms, 100ms, 200ms
let backoff_ms = INITIAL_BACKOFF_MS * (1 << (attempt - 1));
std::thread::sleep(std::time::Duration::from_millis(backoff_ms));
}
Err(err) => return Err(err),
}
}
}
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.2.1 x -- echo |
21.7 ± 0.8 | 20.5 | 31.6 | 1.00 |
mise x -- echo |
22.7 ± 0.6 | 21.0 | 24.6 | 1.05 ± 0.05 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.2.1 env |
21.1 ± 0.7 | 19.9 | 27.4 | 1.00 |
mise env |
21.7 ± 0.6 | 20.1 | 25.5 | 1.03 ± 0.05 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.2.1 hook-env |
21.7 ± 0.5 | 20.6 | 23.2 | 1.00 |
mise hook-env |
22.2 ± 0.6 | 21.0 | 25.1 | 1.03 ± 0.04 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.2.1 ls |
19.5 ± 0.5 | 18.5 | 21.0 | 1.00 |
mise ls |
20.1 ± 0.6 | 19.0 | 22.3 | 1.03 ± 0.04 |
xtasks/test/perf
| Command | mise-2026.2.1 | mise | Variance |
|---|---|---|---|
| install (cached) | 115ms | 116ms | +0% |
| ls (cached) | 72ms | 73ms | -1% |
| bin-paths (cached) | 77ms | 76ms | +1% |
| task-ls (cached) | 557ms | 559ms | +0% |
### 🚀 Features - **(asset-matcher)** enable `mingw-w64` detection for windows packages by @lchagnoleau in [#7981](#7981) - **(crates/vfox)** add download_path to BackendInstall context by @malept in [#7959](#7959) - **(python)** rework `python.uv_venv_auto` setting by @halms in [#7905](#7905) - add "Did you mean?" suggestions and inactive tool warnings by @jdx in [#7965](#7965) ### 🐛 Bug Fixes - **(hook-env)** skip remote version fetching for uninstalled tools in prefer-offline mode by @jdx in [#7976](#7976) - **(install.sh)** Corret `setup` to `set up` by @gogolok in [#7980](#7980) - retry spawn on ETXTBSY (Text file busy) by @jdx in [#7964](#7964) - improve ToolOptions parsing to support comma separated values by @roele in [#7971](#7971) ### 📚 Documentation - improve plugin documentation with comparisons and template links by @jdx in [#7962](#7962) ### 📦️ Dependency Updates - bump hyper-util, system-configuration, lru, aws-sdk, and others by @jdx in [#7977](#7977) ### Chore - **(vfox)** add LuaCATS type definitions for plugin IDE support by @jdx in [#7961](#7961) - **(vfox)** add `download_path` to `BackendInstallCtx` type defintion by @malept in [#7973](#7973) - add stylua linting for vfox plugin Lua files by @jdx in [#7960](#7960) - use system Rust for PPA builds on Ubuntu 26.04+ by @jdx in [#7956](#7956) ### New Contributors - @gogolok made their first contribution in [#7980](#7980) ## 📦 Aqua Registry Updates #### New Packages (4) - [`autobrr/mkbrr`](https://github.com/autobrr/mkbrr) - [`dodobrands/Peekie`](https://github.com/dodobrands/Peekie) - [`grpc/grpc-java/protoc-gen-grpc-java`](https://github.com/grpc/grpc-java/protoc-gen-grpc-java) - [`str4d/age-plugin-yubikey`](https://github.com/str4d/age-plugin-yubikey) #### Updated Packages (3) - [`biomejs/biome`](https://github.com/biomejs/biome) - [`rust-cross/cargo-zigbuild`](https://github.com/rust-cross/cargo-zigbuild) - [`siderolabs/talos`](https://github.com/siderolabs/talos)
## Summary - Adds ETXTBSY retry logic with exponential backoff (50ms, 100ms, 200ms) to `CmdLineRunner::spawn()`, fixing flaky CI failures like [this one](https://github.com/jdx/mise/actions/runs/21591419139/job/62212264595) where `bun -v` fails with "Text file busy (os error 26)" immediately after extraction - This is a race condition on Linux where `execve()` returns ETXTBSY if a file descriptor from the archive extraction isn't fully closed yet - Covers both the normal and raw execution paths in `CmdLineRunner` ## Test plan - [x] `cargo check` passes - [x] `cargo clippy` passes - [x] All lint checks pass - [ ] CI passes (the flake is non-deterministic so absence of failure is the test) 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: small, localized change to process spawning behavior with a bounded retry/backoff; main risk is slightly increased latency or masking repeated spawn failures on Unix. > > **Overview** > Improves command execution robustness by retrying `Command::spawn()` when it fails with Unix `ETXTBSY` (text file busy), using a small exponential backoff. > > Both the normal `execute()` path and the `execute_raw()` path now use `spawn_with_etxtbsy_retry()` instead of spawning once, reducing flaky failures when running freshly extracted binaries. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c631b8f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
### 🚀 Features - **(asset-matcher)** enable `mingw-w64` detection for windows packages by @lchagnoleau in [jdx#7981](jdx#7981) - **(crates/vfox)** add download_path to BackendInstall context by @malept in [jdx#7959](jdx#7959) - **(python)** rework `python.uv_venv_auto` setting by @halms in [jdx#7905](jdx#7905) - add "Did you mean?" suggestions and inactive tool warnings by @jdx in [jdx#7965](jdx#7965) ### 🐛 Bug Fixes - **(hook-env)** skip remote version fetching for uninstalled tools in prefer-offline mode by @jdx in [jdx#7976](jdx#7976) - **(install.sh)** Corret `setup` to `set up` by @gogolok in [jdx#7980](jdx#7980) - retry spawn on ETXTBSY (Text file busy) by @jdx in [jdx#7964](jdx#7964) - improve ToolOptions parsing to support comma separated values by @roele in [jdx#7971](jdx#7971) ### 📚 Documentation - improve plugin documentation with comparisons and template links by @jdx in [jdx#7962](jdx#7962) ### 📦️ Dependency Updates - bump hyper-util, system-configuration, lru, aws-sdk, and others by @jdx in [jdx#7977](jdx#7977) ### Chore - **(vfox)** add LuaCATS type definitions for plugin IDE support by @jdx in [jdx#7961](jdx#7961) - **(vfox)** add `download_path` to `BackendInstallCtx` type defintion by @malept in [jdx#7973](jdx#7973) - add stylua linting for vfox plugin Lua files by @jdx in [jdx#7960](jdx#7960) - use system Rust for PPA builds on Ubuntu 26.04+ by @jdx in [jdx#7956](jdx#7956) ### New Contributors - @gogolok made their first contribution in [jdx#7980](jdx#7980) ## 📦 Aqua Registry Updates #### New Packages (4) - [`autobrr/mkbrr`](https://github.com/autobrr/mkbrr) - [`dodobrands/Peekie`](https://github.com/dodobrands/Peekie) - [`grpc/grpc-java/protoc-gen-grpc-java`](https://github.com/grpc/grpc-java/protoc-gen-grpc-java) - [`str4d/age-plugin-yubikey`](https://github.com/str4d/age-plugin-yubikey) #### Updated Packages (3) - [`biomejs/biome`](https://github.com/biomejs/biome) - [`rust-cross/cargo-zigbuild`](https://github.com/rust-cross/cargo-zigbuild) - [`siderolabs/talos`](https://github.com/siderolabs/talos)
Summary
CmdLineRunner::spawn(), fixing flaky CI failures like this one wherebun -vfails with "Text file busy (os error 26)" immediately after extractionexecve()returns ETXTBSY if a file descriptor from the archive extraction isn't fully closed yetCmdLineRunnerTest plan
cargo checkpassescargo clippypasses🤖 Generated with Claude Code
Note
Low Risk
Low risk: small, localized change to process spawning behavior with a bounded retry/backoff; main risk is slightly increased latency or masking repeated spawn failures on Unix.
Overview
Improves command execution robustness by retrying
Command::spawn()when it fails with UnixETXTBSY(text file busy), using a small exponential backoff.Both the normal
execute()path and theexecute_raw()path now usespawn_with_etxtbsy_retry()instead of spawning once, reducing flaky failures when running freshly extracted binaries.Written by Cursor Bugbot for commit c631b8f. This will update automatically on new commits. Configure here.