Skip to content

fix(spur-cli): accept Slurm's full time grammar for --begin and --deadline - #556

Draft
maybeharshit wants to merge 1 commit into
ROCm:mainfrom
maybeharshit:fix/Issue548
Draft

fix(spur-cli): accept Slurm's full time grammar for --begin and --deadline#556
maybeharshit wants to merge 1 commit into
ROCm:mainfrom
maybeharshit:fix/Issue548

Conversation

@maybeharshit

Copy link
Copy Markdown
Contributor

Motivation

parse_datetime_arg understood only now, now+<count> with an explicit seconds/minutes/hours/days suffix, and ISO 8601 carrying an offset. Every other form sbatch(1) documents was refused at submit time — including three of the four examples printed on the man page. Because the failure is client-side, a script migrating from Slurm breaks immediately rather than scheduling at the wrong time.

Fixes #548.

Technical Details

The grammar moves to a new spur_core::time_spec module. It takes now as a parameter instead of reading the clock, which is what makes it testable: naked forms have to resolve in the submitting host's zone to match Slurm, and passing in both the instant and the offset keeps expectations exact rather than dependent on the runner's clock or TZ.

Newly accepted: bare offsets (read as seconds, as the man page specifies), weeks, HH:MM[:SS] with optional AM/PM, the named times (midnight, elevenses, noon, fika, teatime), today/tomorrow, MMDDYY, MM/DD/YY, MM/DD/YYYY, YYYY-MM-DD, and YYYY-MM-DD[THH:MM[:SS]] with no offset. Anything carrying an explicit offset is still taken verbatim ahead of any local reading, so every input that parsed before parses unchanged.

Decisions worth flagging:

[1] Naked times resolve in the local zone, matching Slurm. --begin=16:00 means 4pm where the user is. This is the point of the fix — reading them as UTC would accept the syntax while scheduling at the wrong moment, which is worse than refusing it.

[2] A time of day that has passed moves to the next day, per the man page. Forms that name a date do not move, so today cannot silently become tomorrow, and a date in the past still starts the job immediately, which is what Slurm does.

[3] Two-digit years expand to 20YY. A scheduler only ever defers into the future, so the 1900s are never the intended reading. chrono's own handling was unusable here: its %Y accepts a short year, so %m/%d/%Y read 08/15/26 as year 26, and its two-digit %y rule does not apply without separators, so 081526 came out as year 26 as well. Both short forms are split by hand instead.

[4] Ambiguous wall-clock times from a daylight-saving fall-back resolve to the earlier instant, and the roll-forward advances the calendar day and re-resolves rather than adding 24 hours, so a DST shift cannot drag the wall-clock time with it.

The scope is deliberately limited to --begin/--deadline. sacct.rs and sreport.rs carry their own byte-identical parse_time_arg for --starttime/--endtime, which accepts a narrower grammar and reads naive timestamps as UTC. Pointing those at the shared module would change what existing sacct invocations mean, so it is left for a separate change.

Test Plan

  • 10 unit tests in spur_core::time_spec against a fixed reference instant in a fixed non-UTC offset, so nothing depends on the runner's clock or zone.
  • Clippy, fmt, and the full workspace suite.
  • Live single-node LXD cluster (Ubuntu 24.04) with TZ=Asia/Kolkata (+05:30), so a naked time misread as UTC would land 5.5 hours away.

Test Result

Clippy and fmt clean; full workspace suite passes.

On the LXD cluster all 30 documented forms were accepted, including every row the issue reports as rejected, while yesterday, now+1fortnight, 25:00, 13pm, and a bare 16 are still refused.

The resulting job states read the parsed values back. now+45, now+60, now+30seconds, now-1hour, today, 2010-01-20T12:34:00, and 2010-01-20T12:34:00Z had all completed within 70 seconds, while now+30minutes, now+1hour, now+2days, now+1week, now+2weeks, tomorrow, and the December dates stayed pending. That split is the check that matters for [1] in the issue: had a bare count been read as minutes, now+45 would still be pending. The already-passed times of day (11:00am, 12pm, noon, elevenses) were pending because they rolled to the next day, while today completed because it did not.

For the timezone, submitting a naked HH:MM one minute ahead in local terms ran at exactly the requested wall-clock time:

local now : 14:38:09+05:30
submitting --begin=14:39

JobState=COMPLETED
StartTime=2026-07-31T09:09:00        # 14:39 IST
ran at 2026-07-31T14:39:00+05:30

Read as UTC that job would have waited until 20:09 local and still been pending.

--deadline shares the parser and accepted 16:00, tomorrow, now+2hours, and 2026-12-25T06:30.

Submission Checklist

Made with Cursor

…dline

parse_datetime_arg understood only now, now+<count> with an explicit
seconds/minutes/hours/days suffix, and ISO 8601 carrying an offset. Every
other form sbatch(1) documents was refused at submit time, so a migrating
script broke immediately rather than scheduling late.

The grammar moves to spur_core::time_spec, which takes `now` as a parameter
rather than reading the clock. That is what makes it testable: naked forms
have to resolve in the submitting host's zone to match Slurm, and pinning
both the instant and the offset keeps the expectations exact instead of
depending on the runner's clock or TZ.

Added: bare offsets (seconds, as sbatch(1) specifies), weeks, HH:MM[:SS]
with optional AM/PM, the named times, today/tomorrow, MMDDYY, MM/DD/YY,
MM/DD/YYYY, YYYY-MM-DD, and YYYY-MM-DD[THH:MM[:SS]] without an offset.
Inputs carrying an explicit offset are still taken verbatim, so everything
that parsed before parses unchanged.

A time of day that has passed moves to the next day per sbatch(1), while
forms naming a date do not, so `today` cannot silently become tomorrow and a
past date still starts the job immediately. Two-digit years expand to 20YY;
chrono's own handling was unusable here, reading both 08/15/26 and 081526 as
year 26.

Co-authored-by: Cursor <cursoragent@cursor.com>
@yansun1996

Copy link
Copy Markdown
Member

Hi @maybeharshit , you marked this PR as draft, are you still working on more changes ?

@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.

Clean, well-structured module. The LocalSpec abstraction and now-injection for testability are well done. No regressions, no over-commenting. Two minor suggestions inline.

Comment on lines +83 to +85
if let Ok(absolute) = DateTime::parse_from_rfc3339(spec) {
return Ok(absolute.with_timezone(&Utc));
}

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_from_rfc3339 requires seconds in the time component. An input like 2026-08-15T06:30Z or 2026-08-15T06:30+05:30 fails RFC 3339, then falls through to parse_datetime which tries %Y-%m-%dT%H:%M but chokes on the trailing Z/+05:30. The user typed an explicit offset, so they clearly mean an absolute instant, but they get the generic "invalid time specification" error instead.

Not a regression (the old parser rejected it too), but since this PR is broadening the grammar, catching the seconds-less offset-aware form would close the gap.

Suggested change
if let Ok(absolute) = DateTime::parse_from_rfc3339(spec) {
return Ok(absolute.with_timezone(&Utc));
}
if let Ok(absolute) = DateTime::parse_from_rfc3339(spec)
.or_else(|_| DateTime::parse_from_str(spec, "%Y-%m-%dT%H:%M:%S%:z"))
.or_else(|_| DateTime::parse_from_str(spec, "%Y-%m-%dT%H:%M%:z"))
{
return Ok(absolute.with_timezone(&Utc));
}

Comment on lines +206 to +216
fn zoned<Tz: TimeZone>(
zone: &Tz,
naive: NaiveDateTime,
input: &str,
) -> Result<DateTime<Tz>, TimeSpecError> {
// Ambiguous wall-clock times (a daylight-saving fall-back) resolve to the
// earlier instant; times inside a spring-forward gap do not exist at all.
zone.from_local_datetime(&naive)
.earliest()
.ok_or_else(|| TimeSpecError::Skipped(input.to_string()))
}

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.

Nit: the Skipped error path (line 215) is implemented but has zero test coverage. FixedOffset can never produce a DST gap, so the suite can't exercise it. The path is simple enough that manual inspection gives confidence, but a short comment noting the gap (or a chrono-tz dev-dependency test) would help future maintainers trust the path stays correct.

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.

--begin and --deadline reject most of Slurm's documented time formats

3 participants