Skip to content

Conversation

@brynary
Copy link
Member

@brynary brynary commented Aug 22, 2025

Summary

  • Adds a configurable --action-timeout CLI argument to control timeouts for operations that rely on external resources
  • Implements timeout handling for network requests, script executions, tool installations, and other external operations
  • Provides user control over timeout duration based on their network conditions and requirements

Changes

CLI Arguments

  • Added --action-timeout flag to init, install, check, and fmt commands
  • Accepts duration formats like "5m" (5 minutes), "300s" (300 seconds), "10m" (10 minutes)
  • Defaults to 5 minutes if not specified

Core Implementation

  • Created duration parsing utility in qlty-cli/src/duration.rs that handles various time formats
  • Added action_timeout field to Settings struct with 5-minute default
  • Applied timeout to:
    • HTTP downloads in qlty-config/src/http.rs
    • Tool installations in qlty-check/src/tool/download.rs
    • Script-based hooks and invocations in qlty-check/src/executor.rs and qlty-check/src/executor/driver.rs

Error Handling

  • Timeout errors respect the --skip-errored-plugins flag
  • Provides clear error messages when operations time out

Benefits

This feature prevents the CLI from hanging indefinitely when:

  • Network resources are slow or unresponsive
  • External scripts enter infinite loops
  • Tool downloads stall due to connectivity issues

Users can adjust the timeout based on their specific needs:

  • Increase for slower network connections
  • Decrease for faster feedback in CI/CD environments

Test Plan

  • Test with default timeout (5 minutes)
  • Test with custom timeout values (e.g., --action-timeout=30s, --action-timeout=10m)
  • Test timeout triggering with slow network simulation
  • Test that --skip-errored-plugins properly handles timeout errors
  • Verify timeout applies to all external operations (downloads, scripts, installations)
  • Test invalid duration formats produce helpful error messages

Note

There is one test failure in tests/cmd/check/timeout.toml that needs investigation. The test expects a timeout to occur but with the new implementation it's not triggering as expected. This may need adjustment to align with the new timeout behavior.

Add --action-timeout CLI argument to control timeouts for operations that rely on external resources (network requests, script executions, tool installations). Default timeout is 5 minutes, configurable with formats like "10m", "300s", etc.

- Add --action-timeout CLI flag to init, install, check, and fmt commands
- Create duration parsing utility supporting various time formats
- Add action_timeout field to Settings struct with 5-minute default
- Apply timeout to HTTP downloads, tool installations, script hooks, and invocations
- Respect --skip-errored-plugins flag for timeout error handling

This prevents hangs when external resources are slow or unresponsive, giving users control over timeout duration based on their needs.
@github-actions
Copy link
Contributor

No issue mentions found. Please mention an issue in the pull request description.

Use GitHub automation to close the issue when a PR is merged

- Add timeout field to ToolBuilder and pass via with_timeout()
- Add timeout fields to DownloadTool and GitHubReleaseTool
- Update Download::install to accept timeout parameter
- Pass timeout to all download operations instead of using thread-local
- Update all runtime tools to pass None for timeout (they don't have configurable timeout yet)
@qltysh
Copy link
Contributor

qltysh bot commented Aug 22, 2025

4 new issues

Tool Category Rule Count
qlty Duplication Found 15 lines of similar code in 2 locations (mass = 80) 4

brynary and others added 3 commits August 22, 2025 09:34
- Changed Download::install to require a timeout parameter (no longer optional)
- Removed redundant timeout fallback logic from each install method
- DownloadTool and GitHubReleaseTool now provide default 10-minute timeout if not configured
- Runtime tools explicitly pass 10-minute timeout for downloads
- Cleaner API ensures there's always a timeout to prevent infinite hangs
- Updated default action_timeout in Settings from 300s (5 min) to 600s (10 min)
- Aligns with the default timeout used for downloads and prepare scripts
- Provides more headroom for slower network connections and larger downloads
- Add timeout field to all runtime tool structs (Go, Java, Node, Python, Ruby, Rust, PHP)
- Pass configured timeout from ToolBuilder to runtime tools instead of hardcoding 600 seconds
- Update Ruby platform-specific implementations to accept timeout parameter
- Use minimum of action timeout and driver timeout to preserve test behavior
- Fix all test code to provide required timeout parameters

This ensures runtime tools respect the CLI-configured action timeout rather than using hardcoded values.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@qltysh
Copy link
Contributor

qltysh bot commented Aug 22, 2025

Diff Coverage for macos-15: The code coverage on the diff in this pull request is 63.2%.

Total Coverage for macos-15: This PR will decrease coverage by 0.06%.

File Coverage Changes
Path File Coverage Δ Indirect
qlty-check/src/executor.rs 0.3
qlty-check/src/executor/driver.rs -0.8
qlty-check/src/planner/plugin.rs 0.1
qlty-check/src/settings.rs 0.8
qlty-check/src/tool/download.rs -1.2
qlty-check/src/tool/github.rs 1.4
qlty-check/src/tool/node.rs 0.0
qlty-check/src/tool/php.rs 0.2
qlty-check/src/tool/php/composer.rs 0.1
qlty-check/src/tool/ruby.rs -1.1
qlty-cli/src/auth/auth_flow.rs -0.6
qlty-cli/src/commands/check.rs -0.1
qlty-cli/src/commands/fmt.rs -1.3
qlty-cli/src/commands/init.rs -0.3
qlty-cli/src/duration.rs 100.0
qlty-config/src/library.rs 0.5
qlty-coverage/src/ci/github.rs -0.3
🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

@qltysh
Copy link
Contributor

qltysh bot commented Aug 22, 2025

Diff Coverage for ubuntu-latest: The code coverage on the diff in this pull request is 63.2%.

Total Coverage for ubuntu-latest: This PR will decrease coverage by 0.07%.

File Coverage Changes
Path File Coverage Δ Indirect
qlty-check/src/executor.rs 0.3
qlty-check/src/executor/driver.rs -0.8
qlty-check/src/planner/plugin.rs 0.1
qlty-check/src/settings.rs 5.0
qlty-check/src/tool/download.rs -1.2
qlty-check/src/tool/github.rs 1.4
qlty-check/src/tool/node.rs 0.0
qlty-check/src/tool/php.rs 0.2
qlty-check/src/tool/php/composer.rs 0.1
qlty-check/src/tool/ruby.rs -1.1
qlty-check/src/tool/tool_builder.rs 5.4
qlty-cli/src/auth/auth_flow.rs -0.6
qlty-cli/src/commands/check.rs -0.1
qlty-cli/src/commands/fmt.rs -1.3
qlty-cli/src/commands/init.rs -0.3
qlty-cli/src/duration.rs 100.0
qlty-config/src/library.rs 0.5
qlty-coverage/src/ci/github.rs -0.3
🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

- Make timeout required (not Option) in GitHubReleaseTool and DownloadTool
- Remove hardcoded 600-second fallbacks throughout codebase
- Use Settings::default().action_timeout as single source of truth
- Update ToolBuilder to always provide timeout value
- Fix all test code to use Settings default instead of hardcoded values
- Ensure timeout flows: CLI → Settings → ToolBuilder → Tools → Methods

This eliminates scattered hardcoded timeout values and ensures the CLI-configured
timeout is properly threaded through the entire system.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Change action_timeout from Option<Duration> to Duration in Settings struct
- Remove Some() wrappers when assigning timeout values in CLI commands
- Eliminate 16+ unwrap() calls throughout the codebase
- Simplify conditional logic in executor/driver.rs
- Update all test code to use non-optional timeout

This ensures Settings always has a valid timeout value (default 10 minutes),
eliminating potential runtime panics and making the code cleaner and more type-safe.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Extract DEFAULT_ACTION_TIMEOUT_SECS constant to eliminate magic number
- Make timeout required in ToolBuilder struct (Duration instead of Option<Duration>)
- Update all ToolBuilder methods to use required timeout field directly
- Remove Option wrapping in install.rs and planner/plugin.rs
- Make Executor::install_tools accept Duration instead of Option<Duration>
- Simplify Install::install method signature to use Duration directly

This makes the code cleaner and more type-safe by eliminating unnecessary
Option handling since timeout is always present in Settings.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Comment on lines +221 to 234
pub fn new_tool(version: &str, timeout: std::time::Duration) -> Box<dyn Tool> {
let platform_tool = sys::platform::Ruby::default();
if Self::binary_install_enabled(&platform_tool) {
Box::new(Self {
version: version.to_string(),
timeout,
platform_tool,
})
} else {
Box::new(RubySource {
version: version.to_string(),
timeout,
})
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Found 15 lines of similar code in 2 locations (mass = 80) [qlty:similar-code]

Comment on lines +238 to 251
pub fn new_runtime(version: &str, timeout: std::time::Duration) -> Box<dyn RuntimeTool> {
let platform_tool = sys::platform::Ruby::default();
if Self::binary_install_enabled(&platform_tool) {
Box::new(Self {
version: version.to_string(),
timeout,
platform_tool,
})
} else {
Box::new(RubySource {
version: version.to_string(),
timeout,
})
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Found 15 lines of similar code in 2 locations (mass = 80) [qlty:similar-code]

Comment on lines +164 to 192
fn runtime_tool(&self, runtime: Runtime, version: &str) -> Box<dyn RuntimeTool> {
let timeout = self.timeout;
match runtime {
Runtime::Node => Box::new(node::NodeJS {
version: version.to_string(),
timeout,
}),
Runtime::Python => Box::new(python::Python {
version: version.to_string(),
timeout,
}),
Runtime::Ruby => ruby::Ruby::new_runtime(version),
Runtime::Ruby => ruby::Ruby::new_runtime(version, timeout),
Runtime::Go => Box::new(go::Go {
version: version.to_string(),
timeout,
}),
Runtime::Rust => Box::new(rust::Rust {
version: version.to_string(),
timeout,
}),
Runtime::Java => Box::new(java::Java {
version: version.to_string(),
timeout,
}),
Runtime::Php => Box::new(php::Php {
version: version.to_string(),
timeout,
}),
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Found 30 lines of similar code in 2 locations (mass = 185) [qlty:similar-code]

Comment on lines +196 to 224
fn release_runtime_tool(&self, runtime: Runtime, version: &str) -> Box<dyn Tool> {
let timeout = self.timeout;
match runtime {
Runtime::Node => Box::new(node::NodeJS {
version: version.to_string(),
timeout,
}),
Runtime::Python => Box::new(python::Python {
version: version.to_string(),
timeout,
}),
Runtime::Ruby => ruby::Ruby::new_tool(version),
Runtime::Ruby => ruby::Ruby::new_tool(version, timeout),
Runtime::Go => Box::new(go::Go {
version: version.to_string(),
timeout,
}),
Runtime::Rust => Box::new(rust::Rust {
version: version.to_string(),
timeout,
}),
Runtime::Java => Box::new(java::Java {
version: version.to_string(),
timeout,
}),
Runtime::Php => Box::new(php::Php {
version: version.to_string(),
timeout,
}),
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Found 30 lines of similar code in 2 locations (mass = 185) [qlty:similar-code]

self.plan.tools(),
self.plan.jobs,
self.progress.clone(),
self.plan.settings.action_timeout,
Copy link
Member Author

Choose a reason for hiding this comment

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

This is a message chain smell.

In keeping with should add the action timeout to the Plan so that the Executor does not need to reach through the Plan to the Settings.

name: String,
tool: Box<dyn Tool>,
progress: Progress,
_timeout: std::time::Duration,
Copy link
Member Author

Choose a reason for hiding this comment

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

Can we remove this _timeout arg?

let pids = handle.pids();
let timeout = plan.driver.timeout;
// Use the minimum of action_timeout and driver timeout
let timeout = std::cmp::min(plan.settings.action_timeout.as_secs(), plan.driver.timeout);
Copy link
Member Author

Choose a reason for hiding this comment

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

The calculation of the effective timeout should be moved from the executor to the planner

let timeout = plan.settings.action_timeout;

// Start the command
let handle = cmd.start().with_context(|| {
Copy link
Member Author

Choose a reason for hiding this comment

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

Can we move the timeout handling down into the cmd code? It looks like we already have a CmdWrapper of some sort.

use std::{fmt::Formatter, path::PathBuf};
use std::{fmt::Formatter, path::PathBuf, time::Duration};

const DEFAULT_ACTION_TIMEOUT_SECS: u64 = 600; // 10 minutes
Copy link
Member Author

Choose a reason for hiding this comment

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

Are there any plugins which should get a higher timeout than 10 mins by default?

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