Skip to content

fix(core)!: read -t MM:SS as minutes:seconds and accept bare days-hours - #811

Open
sunxxuns wants to merge 2 commits into
ROCm:mainfrom
sunxxuns:fix/slurm-time-grammar
Open

fix(core)!: read -t MM:SS as minutes:seconds and accept bare days-hours#811
sunxxuns wants to merge 2 commits into
ROCm:mainfrom
sunxxuns:fix/slurm-time-grammar

Conversation

@sunxxuns

@sunxxuns sunxxuns commented Sep 2, 2026

Copy link
Copy Markdown

Fixes #804.

What was wrong

Slurm documents six accepted --time formats: minutes, minutes:seconds, hours:minutes:seconds, days-hours, days-hours:minutes, days-hours:minutes:seconds. Two were parsed incorrectly, and both failures were silent.

The two-field colon form was read as hours:minutes, so every such limit came out 60x longer than requested. -t 4:00 means four minutes; it was granting four hours, and scontrol show job reported TimeLimit=04:00:00, which looks entirely deliberate.

Bare days-hours failed to parse at all. The remainder after days- went to a helper requiring at least two colon-separated fields, so 2-12 returned None — the same value that means INFINITE — producing a job with no time limit from a request for two and a half days.

Measured against the documented grammar before this change:

-t INPUT     PARSED AS              SLURM MEANS              RATIO
10           600s (10m0s)           600s (10m0s)                1x
4:00         14400s (4h0m)          240s (4m0s)                60x  <== MISMATCH
30:00        108000s (1d6h0m)       1800s (30m0s)              60x  <== MISMATCH
1:30         5400s (1h30m)          90s (1m30s)                60x  <== MISMATCH
0:30         1800s (30m0s)          30s (30s)                  60x  <== MISMATCH
1:00:00      3600s (1h0m)           3600s (1h0m)                1x
0:00:10      10s (10s)              10s (10s)                   1x
2-12         NO LIMIT               216000s (2d12h0m)          INF  <== MISMATCH
1-0          NO LIMIT               86400s (1d0h0m)            INF  <== MISMATCH
2-12:30      217800s (2d12h30m)     217800s (2d12h30m)          1x
2-12:30:15   217815s (2d12h30m)     217815s (2d12h30m)          1x

days-hours:minutes and days-hours:minutes:seconds were already correct, because a colon in the remainder satisfied that helper. Only the colon-less form fell through, which is why this went unnoticed.

What this changes

  • The two-field arm in both parse_slurm_time_seconds and parse_slurm_time_minutes now reads MM:SS. The minutes variant rounds a sub-minute remainder up, matching what the suffixed-duration path already did.
  • parse_hms_seconds and parse_hms accept bare hours after days-, which is what days-hours needs. Both helpers are only reachable from the days- branch, so this does not affect other forms.
  • Doc comments that described H:MM as intended are corrected, including parse_reservation_duration in scontrol.rs, whose comment documented the quirk as something to work around.

Tests

Three existing assertions encoded the old reading and are updated: config.rs test_parse_time and test_parse_time_seconds, and spur-tests t52_2. Added a test per parser covering all six documented forms.

Two changes worth calling out for review:

  • t52_13_build_partitions used a fixture of max_time = "1:00" while asserting 60 minutes — a config author writing the two-field form to mean one hour, which is exactly the mistake this change prevents. Changed to "1:00:00" so the fixture states the intent it always had.
  • parse_time_rejects_overflowing_slurm_durations exercised overflow on the two-field arm with "71582789:00", which only overflowed because minutes were multiplied by 3600. That arm now takes minutes directly, so the input no longer overflows; replaced with "4294967295:01", where the round-up pushes past u32::MAX, plus a value that does not fit u32 at all. I deliberately did not assert that u32::MAX minutes parses successfully, since accounting::INFINITE is u32::MAX and that would invite a sentinel collision.

cargo test --workspace, cargo fmt --all --check and cargo clippy --workspace --all-targets are clean.

Breaking change

Two-field colon values now resolve to 1/60th of their previous duration. This affects -t/--time on sbatch, srun and salloc, and partition MaxTime/DefaultTime. Bare days-hours now produces a real limit where it previously produced an unlimited job.

The explicit HH:MM:SS form was and remains unambiguous, so that is the migration path for anything relying on the old reading. Partition configs are the case to flag in release notes: a site with MaxTime = "30:00" has been enforcing thirty hours and will start enforcing thirty minutes.

Worth considering as a follow-up, but deliberately not in this PR: a startup warning when a partition time value uses the two-field form, so operators are told rather than surprised.

Deliberately out of scope

parse_wall_time in sacctmgr.rs is a third, independent implementation of the same grammar, serving QOS grpwall/maxwall. It has the same two-field bug, also rejects bare days-hours, and uses unchecked arithmetic. I left it alone because it is a different user-facing surface with its own migration story, and docs/admin-guide/accounting.rst currently documents its behaviour explicitly (grpwall=600 and grpwall=10:00 "both mean ten hours"). Happy to follow up there, or in this PR if you would rather it land together.

Longer term, three parallel implementations of one grammar is what allowed the same defect to exist in two of them. Consolidating them would prevent recurrence, but that felt like more than a bug fix should take on without maintainer input.

Slurm documents six accepted `--time` formats: "minutes",
"minutes:seconds", "hours:minutes:seconds", "days-hours",
"days-hours:minutes" and "days-hours:minutes:seconds".

Two of them were parsed incorrectly. Both time parsers read the two-field
colon form as hours:minutes, so every such limit was 60x longer than
requested -- `-t 4:00` meant four minutes but granted four hours. And bare
`days-hours` failed to parse at all: the remainder after `days-` was handed
to a helper that required at least two colon-separated fields, so `-t 2-12`
returned None, which is the value that also means INFINITE, yielding a job
with no time limit.

`days-hours:minutes` and `days-hours:minutes:seconds` were already correct,
since a colon in the remainder satisfied that helper. Only the colon-less
form fell through.

Fixes the two-field arm in both `parse_slurm_time_seconds` and
`parse_slurm_time_minutes`, teaches `parse_hms_seconds` and `parse_hms` to
accept bare hours after `days-`, and corrects the doc comments that
described the old reading as intended.

Three test assertions encoded the incorrect reading and are updated, along
with a partition fixture whose `max_time = "1:00"` was written to mean one
hour -- the same mistake this change prevents. The overflow test for the
two-field arm needed a new input, because minutes are no longer multiplied
by 3600 and so no longer overflow at the old value.

BREAKING CHANGE: two-field colon time values now mean minutes:seconds
rather than hours:minutes, so they resolve to 1/60th of their previous
duration. This affects `-t`/`--time` on sbatch, srun and salloc, and
partition `MaxTime`/`DefaultTime`. Job scripts and site configs relying on
the old reading should switch to the explicit `HH:MM:SS` form, which was
and remains unambiguous. Bare `days-hours` now produces a real limit where
it previously produced an unlimited job.

Refs ROCm#804

Co-authored-by: Cursor <[email protected]>

Copilot AI 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.

🟡 Changes recommended

The new MM:SS→minutes rounding logic can silently compute an incorrect result for inputs where the seconds field is ≥60, and should be made consistent or reject/normalize such values.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes Spur’s Slurm-compatible --time parsing to match Slurm’s documented grammar, correcting a 60× inflation bug for two-field colon values and adding support for the previously-unparsed days-hours form (which could silently become “no limit” in some paths).

Changes:

  • Interpret the 2-field colon form as MM:SS (not HH:MM) in both the minutes and seconds parsers, with minute-granularity rounding consistent with suffixed durations.
  • Accept bare days-hours by allowing the days- remainder to be parsed as hours-only.
  • Update and expand tests to cover all documented Slurm --time forms and adjust fixtures that previously depended on the incorrect interpretation.
File summaries
File Description
crates/spur-core/src/config.rs Fixes core time parsing semantics, extends days- remainder parsing, updates docs/tests for full Slurm grammar coverage.
crates/spur-tests/src/t52_config.rs Updates/extends config parsing tests and corrects a partition fixture to use an unambiguous HH:MM:SS form.
crates/spur-cli/src/scontrol.rs Updates reservation-duration parser documentation to reflect the corrected shared --time grammar.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread crates/spur-core/src/config.rs Outdated
// remainders round up, matching the suffixed-duration path.
let m: u32 = parts[0].parse().ok()?;
let sec: u32 = parts[1].parse().ok()?;
m.checked_add(if sec > 0 { 1 } else { 0 })

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch, fixed in 4913079. The seconds field isn't constrained to 0..59, so the flat one-minute round-up undercounted anything at or above 60s: 1:90 is 150 seconds but yielded 2 minutes instead of 3.

Rather than div_ceil on the seconds field alone, I sum first and then round the total, which keeps the two parsers consistent by construction:

let total = m.checked_mul(60)?.checked_add(sec)?;
u32::try_from(total.div_ceil(60)).ok()

Widening to u64 for the intermediate also makes the overflow boundary exact instead of relying on the multiply to overflow first. Added parse_time_minutes_carries_an_unnormalised_seconds_field covering 0:59, 0:60, 0:61 and 1:90 as a regression guard.

Comment thread crates/spur-core/src/config.rs Outdated
Comment on lines 2119 to 2121
/// Parse the part after `days-`: bare hours, `hours:minutes`, or
/// `hours:minutes:seconds`. The bare-hours case backs Slurm's `days-hours`.
fn parse_hms(s: &str) -> Option<u32> {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

You're right, that comment was inaccurate — parse_hms also backs the bare hours:minutes:seconds form via the three-field arm of parse_slurm_time_minutes, not just the part after days-. Reworded in 4913079 to describe it as an hours-first duration serving both callers, with the bare-hours case called out as the one that backs days-hours.

Left the sibling parse_hms_seconds doc as-is, since that one really is only reached from the days branch.

The MM:SS minutes parser added a flat one minute whenever the seconds
field was non-zero, which undercounts any input at or above 60 seconds:
"1:90" is 150 seconds but yielded 2 minutes instead of 3. The field is
not constrained to 0..59, so sum first and round up.

Also correct the parse_hms doc comment, which described the helper as
days-only even though it backs the bare hours:minutes:seconds form too.

Co-authored-by: Cursor <[email protected]>
@sunxxuns
sunxxuns force-pushed the fix/slurm-time-grammar branch from 75cc814 to 4913079 Compare September 2, 2026 18:00

@shiv-tyagi shiv-tyagi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the PR. One comment. PTAL.

Comment on lines 2125 to +2135
fn parse_hms(s: &str) -> Option<u32> {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 3 && parts.len() != 2 {
if parts.len() > 3 {
return None;
}
let h: u32 = parts[0].parse().ok()?;
let m: u32 = parts[1].parse().ok()?;
let m: u32 = if parts.len() >= 2 {
parts[1].parse().ok()?
} else {
0
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

parse_hms and parse_hms_seconds parse the same grammar (bare hours / H:M / H:M:S) but round differently. parse_hms_seconds returns exact seconds; parse_hms computes minutes as h*60 + m + (1 if s > 0 else 0), a flat one-minute bump regardless of how large s is.

That's the same class of bug this PR fixes in the MM:SS arm above (an unnormalised seconds field should carry into whole minutes, not add a flat one). parse_hms("0:0:90") gives 1 minute; the correct ceiling is 2 (ceil(90/60)). This is reachable from bare H:MM:SS and from the part after days-, so "2-0:0:90" also undercounts by a minute.

Rather than re-deriving the rounding, parse_hms could delegate to parse_hms_seconds:

fn parse_hms(s: &str) -> Option<u32> {
    u32::try_from(parse_hms_seconds(s)?.div_ceil(60)).ok()
}

This drops the duplicate parsing logic and fixes the rounding inconsistency using the same div_ceil + try_from pattern already used in the MM:SS arm.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #811      +/-   ##
==========================================
+ Coverage   80.15%   80.15%   +0.01%     
==========================================
  Files         184      184              
  Lines       87772    87811      +39     
==========================================
+ Hits        70348    70384      +36     
- Misses      17424    17427       +3     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

-t MM:SS is parsed as HH:MM (60x too long) and bare -t days-hours silently yields no time limit

4 participants