-
Notifications
You must be signed in to change notification settings - Fork 181
Added tool timeout for when it takes too long to run pre-check #874
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughAdds a 60-second timeout to subprocess.run in ToolsetCommandPrerequisite, includes stdout/stderr in CalledProcessError diagnostics when present, and handles subprocess.TimeoutExpired by marking the prerequisite failed and reporting partial outputs with a timeout notice. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Caller
participant Prereq as ToolsetCommandPrerequisite
participant Sub as Subprocess
Caller->>Prereq: run()
Prereq->>Sub: execute command (timeout=60s)
alt Success (exit 0)
Sub-->>Prereq: exit 0 + stdout/stderr
Prereq-->>Caller: success
else Non-zero exit (CalledProcessError)
Sub-->>Prereq: non-zero exit + stdout/stderr
Note right of Prereq #f9f0c1: include return code, stdout, stderr in error message
Prereq-->>Caller: failure with diagnostics
else TimeoutExpired
Sub-->>Prereq: timeout + partial stdout/stderr
Note right of Prereq #fdecea: report 60s timeout and partial outputs
Prereq-->>Caller: failure with timeout notice
end
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews: pre_merge_checks: custom_checks: - name: "Undocumented Breaking Changes" mode: "warning" instructions: | Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal). Please share your feedback with us on this Discord post. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
holmes/core/tools.py (1)
228-231
: Fix __invoke_script return type and remove unnecessary type: ignore
__invoke_script
returns(output, return_code, rendered_script)
but is annotated as-> str
, which contradicts the actual return and forces# type: ignore
at the call-site. This will trip mypy against the project’s typing guidelines.- raw_output, return_code, invocation = self.__invoke_script(params) # type: ignore + raw_output, return_code, invocation = self.__invoke_script(params)-def __invoke_script(self, params) -> str: +def __invoke_script(self, params) -> Tuple[str, int, str]: @@ - return output, return_code, rendered_script # type: ignore + return output, return_code, rendered_scriptAlso applies to: 282-300
🧹 Nitpick comments (3)
holmes/core/tools.py (3)
432-433
: Avoid magic-number timeout; centralize it and align the timeout messageNice addition. To prevent drift between the
timeout
argument and the error message (and to make it configurable), single-source the timeout via a module-level constant. While here, prefer showing the fully interpolated command in the timeout error for easier repro, and truncate very large partial outputs to keep status logs readable.Apply this diff:
- timeout=60, + timeout=PREREQUISITE_TIMEOUT_SECONDS, ... - except subprocess.TimeoutExpired as e: + except subprocess.TimeoutExpired as e: self.status = ToolsetStatusEnum.FAILED - output_info = ( - f"\nPartial stdout: {e.stdout}\nPartial stderr: {e.stderr}" - if e.stdout or e.stderr - else "" - ) - self.error = ( - f"`{prereq.command}` timed out after 60 seconds{output_info}" - ) + stdout = (e.stdout or "") + stderr = (e.stderr or "") + max_len = 10_000 + if len(stdout) > max_len or len(stderr) > max_len: + stdout = stdout[:max_len] + "... [truncated]" + stderr = stderr[:max_len] + "... [truncated]" + output_info = f"\nPartial stdout: {stdout}\nPartial stderr: {stderr}" + self.error = ( + f"`{command}` timed out after {PREREQUISITE_TIMEOUT_SECONDS} seconds{output_info}" + )Add this near the imports to define a single source of truth (optionally configurable via env):
# at module top PREREQUISITE_TIMEOUT_SECONDS = int(os.getenv("HOLMES_PREREQ_TIMEOUT_SECONDS", "60"))Also applies to: 450-459
442-449
: Great diagnostics; prefer interpolated command and truncate outputs to avoid log bloatIncluding stdout/stderr is helpful. Two small improvements:
- Use the interpolated command (
command
) rather than the template (prereq.command
) in the error for easier repro.- Truncate very large outputs to avoid flooding the status/error UI.
- output_info = ( - f"\nStdout: {e.stdout}\nStderr: {e.stderr}" - if e.stdout or e.stderr - else "" - ) - self.error = ( - f"`{prereq.command}` returned {e.returncode}{output_info}" - ) + stdout = (e.stdout or "") + stderr = (e.stderr or "") + max_len = 10_000 + if len(stdout) > max_len or len(stderr) > max_len: + stdout = stdout[:max_len] + "... [truncated]" + stderr = stderr[:max_len] + "... [truncated]" + output_info = f"\nStdout: {stdout}\nStderr: {stderr}" + self.error = ( + f"`{command}` returned {e.returncode}{output_info}" + )
474-476
: Ensure config type aligns with the CallablePrerequisite contractDefaulting None to {} is good. To avoid runtime surprises when
self.config
is set to a non-dict (its type isOptional[Any]
), guard the type before passing it to the callable which expectsdict[str, Any]
.- config = self.config if self.config is not None else {} - (enabled, error_message) = prereq.callable(config) + cfg = self.config + config: dict[str, Any] = cfg if isinstance(cfg, dict) else {} + if cfg is not None and not isinstance(cfg, dict): + logging.debug( + "CallablePrerequisite expected dict config; got %s", + type(cfg).__name__, + ) + (enabled, error_message) = prereq.callable(config)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
holmes/core/tools.py
(3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py
📄 CodeRabbit Inference Engine (CLAUDE.md)
**/*.py
: ALWAYS place Python imports at the top of the file, not inside functions or methods
Type hints are required (project is type-checked with mypy)
Use Ruff for formatting and linting (configured in pyproject.toml)
Files:
holmes/core/tools.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: llm_evals
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (5)
holmes/core/tools.py (5)
442-449
: Include the expanded command in error messages for better diagnosability.When a check fails, showing the post-interpolation command (command) is more actionable than the original template (prereq.command).
- self.error = ( - f"`{prereq.command}` returned {e.returncode}{output_info}" - ) + self.error = ( + f"`{command}` returned {e.returncode}{output_info}" + )
450-459
: Reuse the computed timeout and show the expanded command in timeout errors.This keeps messages consistent and accurate if the timeout is made configurable.
- self.error = ( - f"`{prereq.command}` timed out after 60 seconds{output_info}" - ) + self.error = ( + f"`{command}` timed out after {prereq_timeout} seconds{output_info}" + )
425-433
: Optional: ensure child processes are terminated on timeout.shell=True may spawn a shell that then spawns children; the default subprocess.run kill on timeout targets only the shell process. If a prerequisite command starts grandchildren, consider starting a new session and killing the process group on timeout. This requires switching from subprocess.run to Popen + communicate and calling os.killpg on TimeoutExpired.
Happy to provide a concrete refactor if needed.
425-433
: Make subprocess timeout configurable and disable stdin promptsNice work adding a timeout to the subprocess invocation. To avoid a magic constant and prevent commands from blocking on stdin prompts, consider these refinements:
• In
holmes/core/tools.py
(around thesubprocess.run
call):- result = subprocess.run( + # Determine timeout: use per-prerequisite override or fall back to module default + prereq_timeout = getattr(prereq, "timeout_seconds", None) or DEFAULT_PREREQ_TIMEOUT_S + result = subprocess.run( command, shell=True, check=True, text=True, + stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - timeout=60, + timeout=prereq_timeout, )• At the top of
holmes/core/tools.py
, add a module-level default:DEFAULT_PREREQ_TIMEOUT_S: int = 60• In the
ToolsetCommandPrerequisite
model (around line 333), add an optional override field:class ToolsetCommandPrerequisite(BaseModel): command: str expected_output: Optional[str] = None timeout_seconds: Optional[int] = Field( default=None, ge=1, description="Override the default command pre-check timeout (seconds)." )All existing instantiations of
ToolsetCommandPrerequisite
(in code and tests) use keyword args forcommand
/expected_output
and will continue to work without changes.
442-459
: Cap and Sanitize Subprocess Error OutputIt’s easy for
e.stdout
/e.stderr
to become very large or leak sensitive data; I recommend truncating and sanitizing before assigning toself.error
:• Add a module-level constant and helper in holmes/core/tools.py:
MAX_ERROR_OUTPUT_CHARS: int = 4096 def _truncate_text(s: Optional[str], max_chars: int = MAX_ERROR_OUTPUT_CHARS) -> str: if not s: return "" return s if len(s) <= max_chars else s[:max_chars] + "... [truncated]"• In both exception handlers (CalledProcessError and TimeoutExpired), build
output_info
with truncated stdout/stderr:- output_info = ( - f"\nStdout: {e.stdout}\nStderr: {e.stderr}" - if e.stdout or e.stderr - else "" - ) + out = _truncate_text(e.stdout) + err = _truncate_text(e.stderr) + output_info = "" + if out: + output_info += f"\nStdout: {out}" + if err: + output_info += f"\nStderr: {err}"• Sanitize the final error string via your toolset’s sanitizer (e.g. the existing
_sanitize_error
in the Git toolset) before storing:- self.error = ( - f"`{prereq.command}` returned {e.returncode}{output_info}" - ) + raw = f"`{prereq.command}` returned {e.returncode}{output_info}" + self.error = self.toolset._sanitize_error(raw)• Repeat the same pattern in the
TimeoutExpired
block.This will (1) limit log/UI spam, (2) reduce risk of leaking tokens or PII, and (3) ensure you don’t accidentally expose sensitive data in error messages.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
holmes/core/tools.py
(2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.py
: ALWAYS place Python imports at the top of the file, not inside functions or methods
Type hints are required (project is type-checked with mypy)
Use Ruff for formatting and linting (configured in pyproject.toml)
Files:
holmes/core/tools.py
🧠 Learnings (1)
📚 Learning: 2025年07月08日T08:45:41.069Z
Learnt from: nherment
PR: robusta-dev/holmesgpt#610
File: .github/workflows/llm-evaluation.yaml:39-42
Timestamp: 2025年07月08日T08:45:41.069Z
Learning: When suggesting improvements to environment variable handling in robusta-dev/holmesgpt, check first if validation logic already exists rather than reimplementing it.
Applied to files:
holmes/core/tools.py
🧬 Code graph analysis (1)
holmes/core/tools.py (1)
holmes/plugins/toolsets/git.py (1)
error
(406-411)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: llm_evals
- GitHub Check: Pre-commit checks
- GitHub Check: Pre-commit checks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (3)
holmes/core/tools.py (3)
441-449
: Make timeout configurable and prevent stdin hangsGood addition. Recommend making the timeout configurable per prerequisite and closing stdin to avoid interactive blocks.
Apply within this hunk:
result = subprocess.run( command, shell=True, check=True, text=True, + stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - timeout=60, + timeout=getattr(prereq, "timeout_seconds", 60), )Add a field to ToolsetCommandPrerequisite (outside this hunk) to keep strong typing and mypy happy:
class ToolsetCommandPrerequisite(BaseModel): command: str # must complete successfully (error code 0) for prereq to be satisfied expected_output: Optional[str] = None # optional timeout_seconds: int = 60 # seconds
458-465
: Harden error message formatting and avoid "None" leakage; prefer interpolated commandAvoid printing "None" and cap output size to keep errors readable. Also show the interpolated command.
- except subprocess.CalledProcessError as e: - self.status = ToolsetStatusEnum.FAILED - output_info = ( - f"\nStdout: {e.stdout}\nStderr: {e.stderr}" - if e.stdout or e.stderr - else "" - ) - self.error = ( - f"`{prereq.command}` returned {e.returncode}{output_info}" - ) + except subprocess.CalledProcessError as e: + self.status = ToolsetStatusEnum.FAILED + def _trim(s: str) -> str: + return s if len(s) <= 10000 else s[:10000] + "... [truncated]" + output_info = "" + if e.stdout: + output_info += f"\nStdout:\n{_trim(e.stdout)}" + if e.stderr: + output_info += f"\nStderr:\n{_trim(e.stderr)}" + self.error = ( + f"`{command}` returned {e.returncode}{output_info}" + )
466-475
: Timeout path: use dynamic timeout value, avoid "None", cap output, and show interpolated commandMake the message reflect the actual timeout and keep partial outputs tidy.
- except subprocess.TimeoutExpired as e: - self.status = ToolsetStatusEnum.FAILED - output_info = ( - f"\nPartial stdout: {e.stdout}\nPartial stderr: {e.stderr}" - if e.stdout or e.stderr - else "" - ) - self.error = ( - f"`{prereq.command}` timed out after 60 seconds{output_info}" - ) + except subprocess.TimeoutExpired as e: + self.status = ToolsetStatusEnum.FAILED + def _trim(s: str) -> str: + return s if len(s) <= 10000 else s[:10000] + "... [truncated]" + output_info = "" + if e.stdout: + output_info += f"\nPartial stdout:\n{_trim(e.stdout)}" + if e.stderr: + output_info += f"\nPartial stderr:\n{_trim(e.stderr)}" + timeout_s = getattr(prereq, "timeout_seconds", 60) + self.error = ( + f"`{command}` timed out after {timeout_s} seconds{output_info}" + )
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
holmes/core/tools.py
(2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.py
: Always place Python imports at the top of the file, not inside functions or methods
Type hints are required (mypy is configured in pyproject.toml)
Files:
holmes/core/tools.py
🧠 Learnings (1)
📚 Learning: 2025年07月08日T08:45:41.069Z
Learnt from: nherment
PR: robusta-dev/holmesgpt#610
File: .github/workflows/llm-evaluation.yaml:39-42
Timestamp: 2025年07月08日T08:45:41.069Z
Learning: When suggesting improvements to environment variable handling in robusta-dev/holmesgpt, check first if validation logic already exists rather than reimplementing it.
Applied to files:
holmes/core/tools.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Pre-commit checks
- GitHub Check: llm_evals
- GitHub Check: Pre-commit checks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (2)
holmes/core/tools.py (2)
671-679
: Make timeout configurable and prevent interactive hangs.Use a per-prereq timeout and close stdin so checks can’t block on prompts.
- result = subprocess.run( + timeout_secs = getattr(prereq, "timeout_seconds", 60) + result = subprocess.run( command, shell=True, check=True, text=True, + stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - timeout=60, + timeout=timeout_secs, )Add the field to the model (outside this hunk):
class ToolsetCommandPrerequisite(BaseModel): - command: str # must complete successfully (error code 0) for prereq to be satisfied - expected_output: Optional[str] = None # optional + command: str # must complete successfully (error code 0) for prereq to be satisfied + expected_output: Optional[str] = None # optional + timeout_seconds: Optional[int] = Field( + default=60, description="Timeout for the prerequisite command" + )
688-695
: Use the interpolated command in error messages.This makes diagnostics clearer than showing the unexpanded template.
- self.error = ( - f"`{prereq.command}` returned {e.returncode}{output_info}" - ) + self.error = ( + f"`{command}` returned {e.returncode}{output_info}" + )
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
holmes/core/tools.py
(2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.py
: Always place Python imports at the top of the file, not inside functions or methods
Type hints are required (mypy is configured in pyproject.toml)
Files:
holmes/core/tools.py
🧠 Learnings (1)
📚 Learning: 2025年07月08日T08:45:41.069Z
Learnt from: nherment
PR: robusta-dev/holmesgpt#610
File: .github/workflows/llm-evaluation.yaml:39-42
Timestamp: 2025年07月08日T08:45:41.069Z
Learning: When suggesting improvements to environment variable handling in robusta-dev/holmesgpt, check first if validation logic already exists rather than reimplementing it.
Applied to files:
holmes/core/tools.py
🧬 Code graph analysis (1)
holmes/core/tools.py (2)
holmes/plugins/toolsets/newrelic.py (2)
error
(50-55)error
(130-135)holmes/plugins/toolsets/git.py (1)
error
(414-419)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: llm_evals
- GitHub Check: Pre-commit checks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
holmes/core/tools.py (1)
701-710
: TimeoutExpired bug: use e.output (not e.stdout) and print the actual timeout; include e.cmd.Using e.stdout here will raise AttributeError and mask the real timeout failure. Also avoid hard-coding "60 seconds".
- except subprocess.TimeoutExpired as e: - self.status = ToolsetStatusEnum.FAILED - output_info = ( - f"\nPartial stdout: {e.stdout}\nPartial stderr: {e.stderr}" - if e.stdout or e.stderr - else "" - ) - self.error = ( - f"`{prereq.command}` timed out after 60 seconds{output_info}" - ) + except subprocess.TimeoutExpired as e: + self.status = ToolsetStatusEnum.FAILED + # TimeoutExpired exposes .output for captured stdout + partial_stdout = getattr(e, "output", None) + partial_stderr = e.stderr + output_info = ( + f"\nPartial stdout: {partial_stdout}\nPartial stderr: {partial_stderr}" + if partial_stdout or partial_stderr + else "" + ) + cmd = getattr(e, "cmd", command) + self.error = f"`{cmd}` timed out after {secs} seconds{output_info}"#!/bin/bash # Verify there are no other TimeoutExpired handlers using e.stdout rg -nP 'except\s+subprocess\.TimeoutExpired.*\n(?s).*e\.stdout' -C2
🧹 Nitpick comments (2)
holmes/core/tools.py (2)
676-684
: Avoid hard-coded 60s; make timeout configurable and reuse the value in errors.Expose a per-prereq timeout and pass it to subprocess.run. This also lets you print the actual value in the TimeoutExpired message.
Apply within this block:
- result = subprocess.run( + secs = getattr(prereq, "timeout_seconds", DEFAULT_PREREQ_TIMEOUT) + result = subprocess.run( command, shell=True, check=True, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - timeout=60, + timeout=secs, )Additions outside this block:
+DEFAULT_PREREQ_TIMEOUT: int = 60 # default prereq timeout (seconds)
And extend the model:
class ToolsetCommandPrerequisite(BaseModel): command: str # must complete successfully (error code 0) for prereq to be satisfied expected_output: Optional[str] = None # optional + timeout_seconds: Optional[int] = Field( + default=None, gt=0, description="Max seconds to wait before failing the prereq" + )
691-700
: Use the executed command from the exception (e.cmd) instead of prereq.command.Improves accuracy when env vars expand or templates modify the command. Keep current stdout/stderr inclusion.
except subprocess.CalledProcessError as e: self.status = ToolsetStatusEnum.FAILED - output_info = ( + output_info = ( f"\nStdout: {e.stdout}\nStderr: {e.stderr}" if e.stdout or e.stderr else "" ) - self.error = ( - f"`{prereq.command}` returned {e.returncode}{output_info}" - ) + cmd = getattr(e, "cmd", command) + self.error = f"`{cmd}` returned {e.returncode}{output_info}"
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
holmes/core/tools.py
(2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.py
: Always place Python imports at the top of the file, not inside functions or methods
All Python code must include type hints (mypy enforced)
Files:
holmes/core/tools.py
holmes/{core,plugins}/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
All tools must return detailed error messages from underlying APIs, including executed command/query, time ranges/parameters, and full API error response; 'no data' responses must specify what was searched and where
Files:
holmes/core/tools.py
🧠 Learnings (1)
📚 Learning: 2025年07月08日T08:45:41.069Z
Learnt from: nherment
PR: robusta-dev/holmesgpt#610
File: .github/workflows/llm-evaluation.yaml:39-42
Timestamp: 2025年07月08日T08:45:41.069Z
Learning: When suggesting improvements to environment variable handling in robusta-dev/holmesgpt, check first if validation logic already exists rather than reimplementing it.
Applied to files:
holmes/core/tools.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: llm_evals
- GitHub Check: Pre-commit checks
Helps in cases like #873