Skip to content

feat: automatically download error diagnostic files for failed jobs - #628

Merged
njzjz merged 8 commits into
deepmodeling:masterfrom
SchrodingersCattt:feat/download-error-files
Aug 29, 2026
Merged

feat: automatically download error diagnostic files for failed jobs#628
njzjz merged 8 commits into
deepmodeling:masterfrom
SchrodingersCattt:feat/download-error-files

Conversation

@SchrodingersCattt

@SchrodingersCattt SchrodingersCattt commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Problem

When a job fails (terminated), the _last_err_file containing the last 1000 bytes of stderr exists on the remote workdir but is never downloaded to local. After clean_jobs() runs, this diagnostic info is permanently lost.

The existing get_last_error_message() method only reads error info during the run (in retry logic), but does not persist it locally.

Solution

Add try_download_error_info() method called in run_submission() after try_download_result() and before clean_jobs():

  1. For each non-finished job, check if {job_hash}_last_err_file exists on remote
  2. Download content to local_root/{job_hash}_last_err_file
  3. Log the error content as WARNING for immediate visibility
  4. Gracefully handle missing files and context exceptions

This ensures error diagnostics survive remote workdir cleanup.

Tests

6 unit tests in test_download_error_info.py.

Summary by CodeRabbit

  • Bug Fixes
    • Error diagnostics are now retrieved even when submission processing encounters unexpected failures or retry exhaustion.
    • Diagnostic download failures no longer mask the original submission error.
    • Downloaded diagnostics are logged and retained locally for unfinished jobs, improving troubleshooting.

Add try_download_error_info() method that downloads the _last_err_file
(containing last 1000 bytes of stderr) from the remote root to local
root for any job that did not finish successfully.

This is called in run_submission() after try_download_result() and before
clean_jobs(), ensuring error diagnostics survive remote workdir cleanup.

Previously, error information was only available via get_last_error_message()
during the run (in retry logic), but was lost after clean_jobs() deleted
the remote workdir. Now the error content is:
1. Written to local_root/{job_hash}_last_err_file
2. Logged as WARNING for immediate visibility in the output

Gracefully handles missing error files and context exceptions.
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. enhancement New feature or request labels Jul 20, 2026
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

Next included review available in 41 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0d6cc3f2-a1ad-454c-8c46-69f4457cdd3f

📥 Commits

Reviewing files that changed from the base of the PR and between 68441a0 and 77bc7b0.

📒 Files selected for processing (6)
  • dpdispatcher/machine.py
  • dpdispatcher/machines/dp_cloud_server.py
  • dpdispatcher/machines/openapi.py
  • dpdispatcher/submission.py
  • tests/test_cloud_job_error.py
  • tests/test_download_error_info.py
📝 Walkthrough

Walkthrough

Submission.run_submission now retrieves unfinished-job error diagnostics from normal and exceptional execution paths. Diagnostic failures are suppressed. Tests cover job states, missing files, machine contexts, cleanup, and retry-exhaustion failures.

Changes

Error diagnostic handling

Layer / File(s) Summary
Diagnostic download implementation
dpdispatcher/submission.py
Submission.try_download_error_info() downloads available last-error files for unfinished jobs, logs their contents, and suppresses per-job download failures.
Failure-path integration and validation
dpdispatcher/submission.py, tests/test_download_error_info.py
run_submission() invokes diagnostic retrieval from its finally flow. Tests cover normal states, missing files, absent machines, cleanup, and retry-exhaustion errors.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Submission
  participant Job
  participant MachineContext
  participant RemoteStorage
  Submission->>Submission: run_submission()
  Submission->>Submission: enter finally cleanup flow
  Submission->>Job: inspect unfinished state
  Submission->>MachineContext: request remote last-error file
  MachineContext->>RemoteStorage: download diagnostic content
  RemoteStorage-->>MachineContext: return file content
  MachineContext-->>Submission: save diagnostic locally and log content
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: automatic downloading of error diagnostic files for failed jobs.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 72.81553% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 49.62%. Comparing base (b23161c) to head (77bc7b0).
⚠️ Report is 8 commits behind head on master.

Files with missing lines Patch % Lines
dpdispatcher/submission.py 73.33% 16 Missing ⚠️
dpdispatcher/machines/openapi.py 62.50% 6 Missing ⚠️
dpdispatcher/machines/dp_cloud_server.py 77.27% 5 Missing ⚠️
dpdispatcher/machine.py 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #628      +/-   ##
==========================================
+ Coverage   48.33%   49.62%   +1.29%     
==========================================
  Files          40       40              
  Lines        3958     4022      +64     
==========================================
+ Hits         1913     1996      +83     
+ Misses       2045     2026      -19     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@njzjz-bot njzjz-bot left a comment

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.

The helper works in isolation, but its integration point does not run on the terminal-failure path, so the advertised diagnostic preservation is not achieved.

Coding agent: Codex
Codex version: codex-cli 0.144.6
Model: gpt-5.6-sol
Reasoning effort: xhigh

Comment thread dpdispatcher/submission.py Outdated
pass
self.handle_unexpected_submission_state()
self.try_download_result()
self.try_download_error_info()

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.

[P1] Run diagnostic persistence on the terminal-failure path

This line is reached only after every preceding handle_unexpected_submission_state() succeeds. When a terminated job exhausts retries, that method raises and run_submission() exits before this call; I reproduced zero calls to try_download_error_info() in that path. The ratio_unfinished path also rewrites killed jobs to JobStatus.finished, so this helper skips them. Move the download into the exception path before re-raising (or use a carefully structured finally that preserves the original exception), and add an integration test that exhausts retries and verifies the local error file is written. The fix spans the failure-handling control flow, so a one-line suggestion here would not be complete.

Wrap the main execution block (while-loop, handle_unexpected, try_download_result)
in try/finally so that try_download_error_info() is always called, even when
handle_unexpected_submission_state() raises RuntimeError after exhausting retries.

Previously, the error diagnostic download was only reachable on the success path,
defeating the purpose of preserving error info for debugging.

Add integration tests verifying error files are downloaded when retries exhaust.

Addresses reviewer feedback from njzjz-bot (P1).
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:M This PR changes 30-99 lines, ignoring generated files. labels Aug 4, 2026
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Aug 4, 2026

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (4)
dpdispatcher/submission.py (3)

262-263: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the no-op finally: pass.

The finally: pass block at lines 262-263 does nothing. Delete it to keep the control flow readable.

♻️ Proposed cleanup
                 else:
                     self.update_submission_state()
                     self.handle_unexpected_submission_state()
-                finally:
-                    pass
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dpdispatcher/submission.py` around lines 262 - 263, Remove the no-op finally:
pass block from the surrounding exception-handling flow in submission handling,
leaving the existing try/except behavior unchanged.

266-273: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log the suppressed diagnostic-download failure.

The bare except Exception: pass hides all failures of try_download_error_info(). try_download_error_info() already suppresses per-job errors, so this handler only catches setup errors such as a missing local_root. Log at debug level so the failure is traceable. Ruff also flags this as S110/BLE001.

♻️ Proposed fix
             try:
                 self.try_download_error_info()
-            except Exception:
-                pass
+            except Exception as e:
+                dlog.debug(f"Failed to download error diagnostics: {e}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dpdispatcher/submission.py` around lines 266 - 273, Update the exception
handler around try_download_error_info() in the finally block to log suppressed
setup failures at debug level, including the exception details. Preserve the
existing best-effort behavior so diagnostic-download errors do not escape, while
replacing the bare silent handler with Ruff-compliant exception logging.

Source: Linters/SAST tools


299-304: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Handle a relative local_root and set an explicit encoding.

If context.local_root is a bare relative name, os.path.dirname(local_err_path) returns an empty string and os.makedirs("") raises FileNotFoundError. The outer except then discards the diagnostic silently. Also pass an explicit encoding so the write does not depend on the platform default.

♻️ Proposed fix
                         local_err_path = os.path.join(
                             self.machine.context.local_root, err_file_name
                         )
-                        os.makedirs(os.path.dirname(local_err_path), exist_ok=True)
-                        with open(local_err_path, "w") as f:
+                        os.makedirs(
+                            self.machine.context.local_root, exist_ok=True
+                        )
+                        with open(local_err_path, "w", encoding="utf-8") as f:
                             f.write(err_content)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dpdispatcher/submission.py` around lines 299 - 304, Update the error-file
writing logic around local_err_path to create the parent directory only when its
derived path is non-empty, so a bare relative context.local_root remains valid.
Open the file with an explicit encoding while preserving the existing
err_content write behavior.
tests/test_download_error_info.py (1)

15-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for the exit_on_submit path.

The new try/finally in run_submission also runs try_download_error_info() when exit_on_submit=True returns early. Add a test that asserts no failure warning and no local error file for jobs in running state on that path. This locks in the behavior discussed in dpdispatcher/submission.py.

Also applies to: 125-131

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_download_error_info.py` around lines 15 - 16, Add a unit test in
TestDownloadErrorInfo covering run_submission with exit_on_submit=True for a job
in the running state. Assert that the path emits no failure warning and does not
create a local error file, while preserving the existing try_download_error_info
coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@dpdispatcher/submission.py`:
- Line 279: Add type annotations to the try_download_error_info method,
including its return type and any parameter annotations required by the
repository’s conventions. Keep the method’s existing behavior unchanged and
follow the annotation style used by nearby methods in
dpdispatcher/submission.py.
- Around line 236-241: Prevent the exit_on_submit success path in the submission
loop from triggering failure diagnostics in the try/finally cleanup. Update
try_download_error_info() to inspect job states and download or warn only for
genuine failure states, excluding running, waiting, and other nonterminal
states; preserve normal diagnostics for failed jobs.

In `@tests/test_download_error_info.py`:
- Around line 188-200: Update the comments in fake_check_all_finished to reflect
that the first call returns True, all subsequent calls also return True, the
while loop is skipped, and the later handle_unexpected_submission_state call
raises. Remove the contradictory description of a False second call and loop
entry while leaving the function behavior unchanged.

---

Nitpick comments:
In `@dpdispatcher/submission.py`:
- Around line 262-263: Remove the no-op finally: pass block from the surrounding
exception-handling flow in submission handling, leaving the existing try/except
behavior unchanged.
- Around line 266-273: Update the exception handler around
try_download_error_info() in the finally block to log suppressed setup failures
at debug level, including the exception details. Preserve the existing
best-effort behavior so diagnostic-download errors do not escape, while
replacing the bare silent handler with Ruff-compliant exception logging.
- Around line 299-304: Update the error-file writing logic around local_err_path
to create the parent directory only when its derived path is non-empty, so a
bare relative context.local_root remains valid. Open the file with an explicit
encoding while preserving the existing err_content write behavior.

In `@tests/test_download_error_info.py`:
- Around line 15-16: Add a unit test in TestDownloadErrorInfo covering
run_submission with exit_on_submit=True for a job in the running state. Assert
that the path emits no failure warning and does not create a local error file,
while preserving the existing try_download_error_info coverage.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9d8666c9-fc17-4996-9641-3245e9473c3e

📥 Commits

Reviewing files that changed from the base of the PR and between e23bb8c and 68441a0.

📒 Files selected for processing (2)
  • dpdispatcher/submission.py
  • tests/test_download_error_info.py

Comment thread dpdispatcher/submission.py
Comment thread dpdispatcher/submission.py Outdated
Comment thread tests/test_download_error_info.py
- Add -> None return annotation to try_download_error_info()
- Only download error diagnostics for terminated/unknown jobs, not running/waiting
  (fixes false 'failed' warnings when exit_on_submit=True triggers finally block)
- Remove no-op 'finally: pass' block
- Fix stale comments in test fake_check_all_finished

Addresses coderabbit review comments.

@njzjz-bot njzjz-bot left a comment

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.

The diagnostic download is still skipped on two real exhausted-retry paths because the new outer try/finally starts too late. The focused tests cover failures inside or after the polling loop, but not the two earlier calls.

Review context: Codex's quota is close to resetting, so I'm deliberately spending the remaining tokens on reviewing the open PR queue now.

Coding agent: Codex
Codex version: codex-cli 0.144.6
Model: gpt-5.6-sol
Reasoning effort: xhigh

if ratio_unfinished > 0.0 and self.check_ratio_unfinished(ratio_unfinished):
self.remove_unfinished_tasks()
break
try:

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.

[P2] Start the diagnostic guard before the initial failure handling

The calls at lines 228 and 233 run before this try. A recovered job that is already terminated with exhausted retries can raise at the first call; a newly submitted job that fails quickly can raise at the second. In both cases try_download_error_info() is never reached, despite the stated goal of preserving diagnostics on exhausted retries.

Move the enclosing try/finally above the first recovery/status handling path, and add a regression where a recovered terminated job raises from the first handle_unexpected_submission_state() call.

@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:M This PR changes 30-99 lines, ignoring generated files. labels Aug 12, 2026

@njzjz-bot njzjz-bot left a comment

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.

One inline correctness finding.

Coding agent: Codex
Codex version: codex-cli 0.149.0
Model: gpt-5.6-sol
Reasoning effort: xhigh

Comment thread dpdispatcher/submission.py Outdated
if job.job_state in (JobStatus.terminated, JobStatus.unknown):
err_file_name = job.job_hash + "_last_err_file"
try:
if self.machine.context.check_file_exists(err_file_name):

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.

[P2] Retrieve diagnostics through the cloud job interface

This generic path cannot find diagnostics for Bohrium/OpenAPI jobs. OpenAPIContext.check_file_exists()/read_file() and DPCloudServerContext.check_file_exists()/read_file() inspect the client-side ~/.dpdispatcher/dp_cloud_server metadata directory, while *_last_err_file is created inside the remote job workspace and is not included in the cloud output-file list. An exhausted cloud job therefore silently skips the advertised diagnostic persistence. Please expose a context-level diagnostic download operation or include and retrieve this artifact through the cloud result/log API.

Coding agent: Codex
Codex version: codex-cli 0.149.0
Model: gpt-5.6-sol
Reasoning effort: xhigh

@njzjz-bot njzjz-bot left a comment

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.

No blocking findings after reviewing the full diff, related code, and CI checks.

Coding agent: Codex
Codex version: codex-cli 0.151.0
Model: gpt-5.6-sol
Reasoning effort: xhigh

@njzjz-bot njzjz-bot left a comment

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.

Reviewed the complete diff and relevant surrounding code. No blocking findings. The relevant CI checks are passing, and targeted tests were run additionally for higher-risk changes where warranted.

Coding agent: Codex
Codex version: codex-cli 0.151.0
Model: gpt-5.6-sol
Reasoning effort: xhigh

@njzjz-bot njzjz-bot left a comment

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.

Independent review completed. I found no blocking issues in the changed behavior, compatibility, or test coverage.

Coding agent: Codex
Codex version: codex-cli 0.151.0
Model: gpt-5.6-sol
Reasoning effort: xhigh

@njzjz
njzjz merged commit c47a38e into deepmodeling:master Aug 29, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants