Troubleshoot the opt job that failed, not the highest-numbered one - #943
Troubleshoot the opt job that failed, not the highest-numbered one#943calvinp0 wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes opt-job troubleshooting in Scheduler so it targets the actual failed optimization job (or, if none is supplied, the most recently spawned job) instead of re-deriving a “latest” job by taking the highest job number—preventing incorrect troubleshooting behavior in restarted projects where job numbers are not globally monotonic.
Changes:
- Pass the failed
jobfromparse_opt_geo()intotroubleshoot_opt_jobs()so troubleshooting operates on the correct job instance. - Add
get_latest_opt_job()(insertion-order “latest”) andget_preceding_opt_job()(identity-based predecessor lookup) to replace job-number-based selection/decrement logic. - Add a focused regression test suite (
TestTroubleshootOptJobIdentity) covering identity selection, insertion-order fallback, predecessor lookup, unregistered jobs, and empty job lists.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| arc/scheduler.py | Updates opt-job troubleshooting to accept the failed job explicitly, and adds helpers to find latest/preceding jobs by insertion order and object identity. |
| arc/scheduler_test.py | Adds regression tests ensuring troubleshooting targets the correct failed opt job in restarted-project scenarios. |
Suppressed comments (1)
arc/scheduler.py:3620
- This signature uses quoted type annotations for
JobAdapter. Since the file hasfrom __future__ import annotationsand already uses unquoted annotations elsewhere, dropping the quotes here keeps typing style consistent.
def get_preceding_opt_job(self, label: str, job: 'JobAdapter') -> 'JobAdapter | None':
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## fix_ts_switch_job_type_guard #943 +/- ##
================================================================
+ Coverage 63.76% 63.80% +0.03%
================================================================
Files 114 114
Lines 38407 38412 +5
Branches 10037 10038 +1
================================================================
+ Hits 24491 24508 +17
+ Misses 11013 10997 -16
- Partials 2903 2907 +4
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
5c2a544 to
a1c74d8
Compare
a1c74d8 to
8318684
Compare
8318684 to
a45e972
Compare
`parse_opt_geo()` calls `troubleshoot_opt_jobs()` on the failed branch while already holding
the job that failed, but passed only the species label. `troubleshoot_opt_jobs()` then
discarded that job and re-derived one by scanning `job_dict[label]['opt']` for the largest
`int(job_name[5:])`.
Job numbers are only monotonic within a single execution. A project that has been restarted
holds jobs from several numbering eras at once, so the highest number can belong to an old
job. One benchmark project's `job_dict` held 102 opt jobs numbered 1332-9779 while the
chronologically newest was `opt_a2098`; troubleshooting therefore selected a stale `opt_a9755`,
found it converged and run with a fine grid, and hit the sanity guard:
SchedulerError: opt job for TS0 seems right, yet "run_opt_job" was called.
That aborts the entire run, discarding hours of completed optimization.
`troubleshoot_opt_jobs()` now accepts the job to troubleshoot and `parse_opt_geo()` passes
the one that failed. The fine/coarse pairing no longer decrements a job number either: the
new `get_preceding_opt_job()` locates the predecessor by the job's identity. When no job is
supplied, `get_latest_opt_job()` resolves recency from the insertion order of `job_dict`
rather than from the job number, and an absent opt job is now logged instead of raising
`AttributeError` on `None`.
a45e972 to
5a202e9
Compare
What went wrong
A production run died with:
parse_opt_geo()reaches troubleshooting only on the failure branch, and it already holds the job that failed — but it passed only the species label:troubleshoot_opt_jobs()then re-derived a job by scanning for the largest job number:Job numbers are only monotonic within a single execution. A project that has been restarted holds jobs from several numbering eras at once, so the highest number can belong to an old job.
In the run that crashed,
job_dict['TS0']['opt']held 102 opt jobs numbered 1332–9779 (22 of them 9xxx), while the chronologically newest wasopt_a2098:opt_a2098— 08-01 00:51opt_a9779— an earlier eraSo when
opt_a2098failed, troubleshooting selected a stale 9xxx job, found it converged and run with a fine grid, and hit the sanity guard — which raises and aborts the entire run, discarding 3.5 hours of completed optimization plus everything queued behind it.The fix
troubleshoot_opt_jobs(label, job=None)accepts the job to troubleshoot;parse_opt_geo()passes the one that actually failed.get_preceding_opt_job()locates the predecessor by the job's identity (candidate is job).get_latest_opt_job()resolves recency from the insertion order ofjob_dict[label]['opt']rather than from the job number, for callers that supply no job.AttributeErroronNone.The
SchedulerErrorguard is deliberately kept. It is a real invariant check, and with the correct job passed it is unreachable from this path — sinceparse_opt_geo()only troubleshoots when the status is not done. Removing it would hide a future bug rather than fix one.Evidence
6 new tests in
TestTroubleshootOptJobIdentity. The central one callstroubleshoot_opt_jobs(label=...)with the original signature, against ajob_dictseeded exactly like the failing project — a staleopt_a9755(done, fine) plus the neweropt_a2098that failed. Without this change it fails with the reported error verbatim:The other five cover insertion-order recency, identity-based predecessor lookup, an unregistered job, and the empty-
job_dictpath.245 tests pass across all four stack layers applied together (
scheduler_test,output_test,species_test,checks/ts_test,job/trsh_test).Why this belongs on this stack
Same root pattern as the three layers below: an integer that merely looks like an ordering, used as identity. #940 was list position vs
TSGuess.index; #941 was atsg<i>adapter number used as a list subscript; this is a job number used as a timestamp. Each is a valid ordering within one context and silently wrong across contexts — and each surfaces as a function that receives a label and re-derives an object the caller already held.