Skip to content

fix: retry spawn on ETXTBSY (Text file busy)#7964

Merged
jdx merged 1 commit intomainfrom
fix/etxtbsy-retry
Feb 2, 2026
Merged

fix: retry spawn on ETXTBSY (Text file busy)#7964
jdx merged 1 commit intomainfrom
fix/etxtbsy-retry

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented Feb 2, 2026

Summary

  • Adds ETXTBSY retry logic with exponential backoff (50ms, 100ms, 200ms) to CmdLineRunner::spawn(), fixing flaky CI failures like this one 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

  • cargo check passes
  • cargo clippy passes
  • All lint checks pass
  • CI passes (the flake is non-deterministic so absence of failure is the test)

🤖 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 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.

Written by Cursor Bugbot for commit c631b8f. This will update automatically on new commits. Configure here.

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>
Copilot AI review requested due to automatic review settings February 2, 2026 13:57
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @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

  • ETXTBSY Retry Logic: Implemented exponential backoff (50ms, 100ms, 200ms) for CmdLineRunner::spawn() to handle ETXTBSY (Text file busy) errors. This specifically addresses a Linux race condition where execve() fails if a file descriptor isn't fully closed immediately after a binary extraction or write operation.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 using nix::errno::Errno on 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))));
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +424 to +430
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);
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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
);

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +423 to +437
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),
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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),
            }
        }
    }

@github-actions
Copy link
Copy Markdown

github-actions bot commented Feb 2, 2026

Hyperfine Performance

mise x -- echo

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%

@jdx jdx merged commit 9171f2c into main Feb 2, 2026
37 checks passed
@jdx jdx deleted the fix/etxtbsy-retry branch February 2, 2026 14:16
mise-en-dev added a commit that referenced this pull request Feb 3, 2026
### 🚀 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)
lucasew pushed a commit to lucasew/CONTRIB-mise that referenced this pull request Feb 18, 2026
## 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>
lucasew pushed a commit to lucasew/CONTRIB-mise that referenced this pull request Feb 18, 2026
### 🚀 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants