fix(spur-cli): accept Slurm's full time grammar for --begin and --deadline - #556
fix(spur-cli): accept Slurm's full time grammar for --begin and --deadline#556maybeharshit wants to merge 1 commit into
Conversation
…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>
|
Hi @maybeharshit , you marked this PR as draft, are you still working on more changes ? |
shiv-tyagi
left a comment
There was a problem hiding this comment.
Clean, well-structured module. The LocalSpec abstraction and now-injection for testability are well done. No regressions, no over-commenting. Two minor suggestions inline.
| if let Ok(absolute) = DateTime::parse_from_rfc3339(spec) { | ||
| return Ok(absolute.with_timezone(&Utc)); | ||
| } |
There was a problem hiding this comment.
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.
| 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)); | |
| } |
| 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())) | ||
| } |
There was a problem hiding this comment.
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.
Motivation
parse_datetime_argunderstood onlynow,now+<count>with an explicitseconds/minutes/hours/dayssuffix, and ISO 8601 carrying an offset. Every other formsbatch(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_specmodule. It takesnowas 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 orTZ.Newly accepted: bare offsets (read as seconds, as the man page specifies),
weeks,HH:MM[:SS]with optionalAM/PM, the named times (midnight,elevenses,noon,fika,teatime),today/tomorrow,MMDDYY,MM/DD/YY,MM/DD/YYYY,YYYY-MM-DD, andYYYY-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:00means 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
todaycannot 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%Yaccepts a short year, so%m/%d/%Yread08/15/26as year 26, and its two-digit%yrule does not apply without separators, so081526came 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.rsandsreport.rscarry their own byte-identicalparse_time_argfor--starttime/--endtime, which accepts a narrower grammar and reads naive timestamps as UTC. Pointing those at the shared module would change what existingsacctinvocations mean, so it is left for a separate change.Test Plan
spur_core::time_specagainst a fixed reference instant in a fixed non-UTC offset, so nothing depends on the runner's clock or zone.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 bare16are 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, and2010-01-20T12:34:00Zhad all completed within 70 seconds, whilenow+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+45would still be pending. The already-passed times of day (11:00am,12pm,noon,elevenses) were pending because they rolled to the next day, whiletodaycompleted because it did not.For the timezone, submitting a naked
HH:MMone minute ahead in local terms ran at exactly the requested wall-clock time:Read as UTC that job would have waited until 20:09 local and still been pending.
--deadlineshares the parser and accepted16:00,tomorrow,now+2hours, and2026-12-25T06:30.Submission Checklist
Made with Cursor