fix(spurctld): enforce partition walltime limits and validate time config - #593
fix(spurctld): enforce partition walltime limits and validate time config#593shiv-tyagi wants to merge 3 commits into
Conversation
…nfig Partition MaxTime was never enforced and invalid time strings in config silently became UNLIMITED. This adds submit-time enforcement, loud config validation, a default-time fallback, and suffixed-duration parsing. - enforce_part_limits (NO/ALL/ANY, default NO) gates submit-time rejection of over-limit jobs; NO preserves the prior admit-and-pend behavior. Time violations now report the specific PartitionTimeLimit pending reason. - SlurmConfig::validate() rejects malformed partition max_time/default_time at load instead of silently dropping the cap. - apply_default_time_limit follows DefaultTime -> MaxTime -> cluster default_time_limit_minutes (default 0 = disabled). The fallback is opt-in, so unbounded jobs stay unbounded on upgrade. - Time strings now accept suffixed durations (1h, 90m, 1h40m, 2d12h, 30s) in addition to Slurm grammar, across config, CLI -t, scontrol, and REST. Slurm grammar is tried first and output still renders HH:MM:SS. A deployed max_time="1h" that was silently UNLIMITED becomes a 60-minute cap. All new enforcement and fallback behavior is opt-in, so an unchanged config behaves exactly as before. Validated with unit and e2e tests and on a 4-node bare-metal cluster. Refs SPUR-122.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #593 +/- ##
==========================================
+ Coverage 76.24% 77.26% +1.01%
==========================================
Files 166 171 +5
Lines 65418 70448 +5030
==========================================
+ Hits 49876 54426 +4550
- Misses 15542 16022 +480 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR closes a Slurm-compat gap in spurctld by enforcing partition wall-time (MaxTime) semantics more correctly, adding submit-time enforcement gating (enforce_part_limits), and making time parsing/validation fail loudly rather than silently degrading to UNLIMITED.
Changes:
- Add
scheduler.enforce_part_limits(NO/ALL/ANY) and enforce partitionMaxTimeat submit when enabled; improve pending reasons to usePartitionTimeLimitfor wall-time violations. - Make config parsing reject malformed partition
max_time/default_timevalues at load; extend time parsing to accept suffixed durations (1h,90m,30s, etc.). - Change cluster-wide defaulting behavior so
scheduler.default_time_limit_minutesdefaults to0(disabled) and only bounds-t-less jobs when explicitly enabled.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/native_host/e2e/test_partitions.py | Adds E2E coverage for submit-time rejection/acceptance when EnforcePartLimits=ALL. |
| docs/admin-guide/configuration.rst | Documents enforce_part_limits, updates the default for default_time_limit_minutes, and notes suffixed duration support. |
| crates/spurctld/src/cluster.rs | Implements submit-time MaxTime enforcement, refines pending-reason selection, and updates default-time fallback behavior. |
| crates/spur-tests/src/t52_config.rs | Updates config-default expectations for default_time_limit_minutes (now 0). |
| crates/spur-core/src/config.rs | Introduces EnforcePartLimits, adds loud partition time validation, and extends time parsing to accept suffixed durations. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let requested_str = | ||
| spur_core::config::format_time(Some(time_limit.num_minutes().max(0) as u32)); | ||
| let max_str = spur_core::config::format_time(offending.max_time_minutes); |
There was a problem hiding this comment.
Fixed in 1e08eff. Added format_time_seconds and render both the requested time and the cap at second precision. A 0:01:30 request against a 00:01:00 cap now reports 00:01:30 exceeds MaxTime 00:01:00 instead of the contradictory 00:01:00.
|
Important (fix before merge)
Fix: for a multi-partition request, derive the default from the minimum MaxTime across requested partitions, or skip ALL enforcement for an auto-derived (non-user-supplied) limit. Add a test pinning the chosen semantics. Minor
Release note Bottom line: issue 1 is the only thing that can produce a genuinely surprising rejection and should be resolved (plus a test) before merge; 2 and 3 are cheap UX/doc wins |
yansun1996
left a comment
There was a problem hiding this comment.
Thanks for closing this gap — the enforcement/validation/duration-parsing work looks solid and stays backward compatible by default. One correctness issue around multi-partition auto-default-time + EnforcePartLimits=ALL should be fixed before merge (see inline); the rest below are smaller polish items and test-coverage suggestions.
A couple things without a good inline anchor:
partition_block's doc comment ("limits rejected by every Up alternative returnPartitionConfig") is now stale — the body can also return the more specificPartitionTimeLimit.- Several new doc comments run 3+ lines (
EnforcePartLimitsenum doc,parse_partition_time/parse_time_minutes/parse_suffix_duration_seconds,apply_default_time_limit) — worth trimming to a line or two each. - The
EnforcePartLimits::Noarm inside thesatisfiedmatch invalidate_partition_time_limitis unreachable (the function already returns early forNo) — harmless, just dead code. - Agree with the existing Copilot comment on the error message's minute- vs. second-precision mismatch.
Missing test coverage worth adding:
- The multi-partition +
ALL+ auto-default-time scenario below. - Case-insensitive parsing of
enforce_part_limits("all","Yes","any") and the reject-on-garbage-value path — theDeserializeimpl looks correct but nothing exercises it. - A partition with both
default_timeand a smallermax_time(plus a nonzero cluster default), to pin thatDefaultTimealways wins in the fallback chain.
Also noting as a follow-up, not a blocker: spec.time_limit can be negative (no non-negativity check on the client-supplied duration), which would trivially satisfy partition_time_allows regardless of mode — pre-existing, adjacent to this PR's changes.
|
Correction on the
So there is no lost 60-minute cap on upgrade:
I kept the default at 0 for that reason. Restoring 60 would itself be the behavior change, since it would start capping |
…tition default from min Address PR review on partition walltime enforcement: - Gate EnforcePartLimits submit-time rejection to a wall-time the user actually requested; an auto-filled default never rejects. Fixes the blocker where a -t-less job to multiple partitions under ALL could be rejected against a partition it never sized itself to. - Derive the auto-default from the minimum resolved limit across all requested partitions so it fits every one; an unbounded requested partition leaves the job unbounded (unchanged upgrade behavior). - Reject a negative user-supplied wall-time at submit. - Format requested time and cap at second precision so a 10m30s request vs a 00:10:00 cap no longer prints a contradictory 00:10:00 message. - Snapshot config and partitions once per submit and thread them through defaulting and enforcement, closing a concurrent-reconfigure race. - Reuse spur_core::partition::matched_partitions instead of a hand-rolled resolve, and drop the unreachable EnforcePartLimits::No arm. - Parse enforce_part_limits case-insensitively (accepting Slurm's YES alias for ALL) and reject unknown values at startup. - Docs: clarify MaxTime pinning for -t-less jobs and reframe the default_time_limit_minutes release note (previously inert, now activated; default 60 to 0 keeps -t-less jobs unbounded). Verified with cargo clippy, cargo test, and an exhaustive bare-metal run (config validation, all enforce modes, default derivation, runtime kill).
|
@yansun1996 I have addressed your comments. Can you please take a look? |
yansun1996
left a comment
There was a problem hiding this comment.
Re-reviewed after the latest fixes — the 5 prior points are all genuinely addressed (verified against the code, not just the commit message). Two smaller things turned up in this pass, left as inline comments: an Up-state filtering gap in validate_partition_time_limit, and the single-snapshot fix being incomplete. Also a doc-validation gap and a couple of doc-comment nits.
…bmit snapshot Second review round on PR ROCm#593: - validate_partition_time_limit now considers only Up partitions, matching partition_block. A Down partition's smaller MaxTime no longer rejects a job that fits an Up partition, and an all-inactive request pends (PartitionInactive) instead of printing a misleading "exceeds MaxTime". - Thread submit_job's single partitions snapshot into validate_partition and validate_partition_node_bounds instead of each re-reading self.partitions, so the whole submit path sees one consistent view under a concurrent reconfigure(). - Reject a partition whose finite default_time exceeds its finite max_time at config load; otherwise a -t-less job is auto-filled past the partition's own cap and pends forever. - Trim the over-long doc comments flagged in review. Adds unit tests for the Up-only enforcement (ignore Down, still reject over-limit Up, ok when all inactive), min-across-DefaultTime derivation, and the default_time > max_time config rejection (plus == and UNLIMITED-max acceptance).
yansun1996
left a comment
There was a problem hiding this comment.
Re-reviewed at d4d9875 — all points from the last round check out (Up-partition filtering, single submit-time snapshot, default>max validation). Nice work tightening this up. Two small non-blocking follow-ups if you want to fold them in: a few new doc comments are still 3+ lines (worth trimming to 1-2), and the admin doc could mention that a malformed max_time/default_time now fails config load at startup instead of silently becoming UNLIMITED. Approving.
| let requested: Vec<&Partition> = | ||
| spur_core::partition::matched_partitions(Some(partition_spec), partitions) | ||
| .into_iter() | ||
| .filter(|p| p.state == spur_core::partition::PartitionState::Up) |
There was a problem hiding this comment.
New "enforce only against Up partitions" scoping diverges from Slurm
underEnforcePartLimits=ALL, filtering to onlyUppartitions means a DOWN/DRAINED partition's (possibly stricter)MaxTimeis silently ignored. Slurm'sEnforcePartLimitsvalidates against the requested partitions regardless of current state. Also, ifnorequested partition is currently Up, the job is admitted completely unchecked (requested.is_empty() => Ok).
enforcement outcome becomes time-dependent on transient node/partition state — the samesbatchis accepted or rejected depending on whether a partition happens to be down at submit. That's surprising and hard to diagnose.
What this fixes
Partition
MaxTimewas never enforced, and invalid time strings in config silently becameUNLIMITED. Jobs could exceed a partition's wall-time cap, and a typo likemax_time = "1h"quietly removed the cap instead of applying it. Refs SPUR-122.This is Group 1 of the fix (submit/schedule-time validation and defaulting). Group 2 (interactive allocation reaping via
InactiveLimit) is a follow-up.Approach
enforce_part_limits(NO/ALL/ANY, defaultNO): gates submit-time rejection of over-limit jobs, mirroring Slurm'sEnforcePartLimits.ALLrejects unless the job fits every requested partition;ANYrejects only when it fits none. DefaultNOkeeps the prior admit-and-pend behavior. Deserialization is case-insensitive and accepts Slurm'sYESalias.PartitionTimeLimitinstead of the genericPartitionConfig.SlurmConfig::validate()rejects malformed partitionmax_time/default_timeat load rather than silently dropping the cap.apply_default_time_limitfollowsDefaultTime -> MaxTime -> scheduler.default_time_limit_minutes. That cluster fallback now defaults to0(disabled), so on upgrade a-t-less job on an unlimited partition stays unbounded. Sites opt into reclamation by setting it> 0.1h,90m,1h40m,2d12h,30sin addition to Slurm grammar, across config, CLI-t,scontrol, and REST. Slurm grammar is tried first (disjoint, no reinterpretation of existing values) and output still rendersHH:MM:SS.Backward compatibility
Non-breaking and backward compatible by default. With an unchanged config (
enforce_part_limits = NO,default_time_limit_minutes = 0) no submit-time rejections are added and no job gets a wall-time it wouldn't have had before. No persisted-state, proto, or CLI/REST-output changes.One intended behavior change worth a release note: a deployed
max_time = "1h"was previously silentlyUNLIMITED; it now parses as a 60-minute cap (honoring intent). A genuinely invalid string (e.g."1 hour") now fails loudly at startup instead of removing the cap.Testing
ALL/ANYaccept/reject incl. no-partition-fits), the default-time fallback branches, config validation, and suffixed-duration parsing (plus updated existing tests). Full suite green; clippy clean.TestEnforcePartLimitsintests/native_host/e2e/test_partitions.py.ALLrejects-t 2h/-t 90mon a 1h partition and accepts-t 45m/-t 1h;default_time = "10m"applies to-t-less jobs;default_time_limit_minutes = 0leaves unlimited-partition jobs unbounded while= 60bounds them;max_time = "1h"loads as a 60-min cap; and"1 hour"fails startup loudly.Review follow-ups
Two rounds of review addressed in later commits:
-t(never an auto-filled default), and the-t-less default derives from the minimum resolved limit across all requested partitions. Error messages render at second precision.Uppartitions (matching the pending-reason path): a Down partition'sMaxTimeno longer rejects a job that fits an Up one, and an all-inactive request pends asPartitionInactiverather than reporting "exceeds MaxTime". This is a minor, intended behavior change underALL/ANY.submit_job's config/partitions snapshot is threaded through all validators, so the whole submit path sees one consistent view under a concurrentreconfigure().validate()rejects a partition whose finitedefault_timeexceeds its finitemax_time(which would otherwise auto-fill a-t-less job past its own cap and pend it forever).Follow-up #604 tracks moving to Slurm-style per-partition default resolution at schedule time.