Repository navigation
fix(scheduler): parse fractional and Pi/Ei resource quantities - #87
Merged
calfonso merged 1 commit intoJul 29, 2026
Merged
Conversation
`parse_resource_quantity` parsed the digits of a suffixed quantity with
`str::parse::<i64>()`, so every `resource.Quantity` carrying a decimal
point silently fell back to `unwrap_or(0)`. The upstream grammar in
`k8s.io/apimachinery/pkg/api/resource/quantity.go` allows a decimal point
with every suffix:
<number> ::= <digits> | <digits>.<digits> | <digits>. | .<digits>
so `"0.5Gi"` is as valid as `"512Mi"`. It parsed as 0, which the
scheduler reads as "this pod requests no memory" — the pod is then
admitted to any node regardless of remaining capacity. The same
`unwrap_or(0)` applied to node `allocatable`, so a node advertising
`"1.5Ti"` looked like it had no memory at all.
`Pi` and `Ei` were missing from the binarySI set entirely and fell
through to the same 0.
Replace the hand-rolled `strip_suffix` chain with a suffix table applied
over an `f64` parse, ordered longest-first so `"1Mi"` cannot be read as
`"1M"`. Bare numbers and the `<decimalExponent>` form are parsed
directly, which keeps `"129e6"` working and lets `"1E"` fall through to
the exa suffix. Non-finite input (`"inf"`, `"1e400"`) is now rejected
rather than saturating to `i64::MAX`, which would have advertised
infinite node capacity.
CPU folds into the same path: `m` is the shared 10^-3 decimalSI suffix,
so `"100m"` is 0.1 cores and converts to 100 millicores without a
special case.
`scheduler.rs` carried a byte-identical copy of the same function; it now
shares the fixed one instead.
Signed-off-by: Vyncint Ng <[email protected]>
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Signed-off-by: Vyncint Ng <[email protected]>
This was referenced Jul 29, 2026
Closed
indyjonesnl
added a commit
to indyjonesnl/k3rs
that referenced
this pull request
Jul 29, 2026
`parse_resource_quantity` parsed the digits of a suffixed quantity with
`str::parse::<i64>()`, so every `resource.Quantity` carrying a decimal
point fell back to `unwrap_or(0)`. The upstream grammar in
`k8s.io/apimachinery/pkg/api/resource/quantity.go` permits a decimal
point with every suffix:
<number> ::= <digits> | <digits>.<digits> | <digits>. | .<digits>
so `"0.5Gi"` is as valid as `"512Mi"` and denotes the same 536870912
bytes. Parsing it as 0 does not fail loudly — it reads as "this pod
requests no memory", so the pod is admitted to any node regardless of
remaining capacity. The same fallback applies to node `allocatable`, so
a node advertising `"1.5Ti"` is accounted as having no memory and
nothing schedules onto it. `Pi`/`Ei` were missing from the binarySI set
and hit the same path. `"inf"` went the other way: it parses as an f64
and `as i64` saturated it to `i64::MAX`, advertising infinite capacity.
Rather than hand-roll a third suffix table, route both scheduler copies
through `rusternetes_common::quantity::Quantity` — the in-tree port of
apimachinery's parser — and account CPU in millicores via
`milli_value()` and everything else via `value()`, mirroring upstream
`Resource.Add` (`pkg/scheduler/framework/types.go:917-918`). That gets
exact i128 arithmetic and upstream's ceiling rounding instead of f64
approximation. `scheduler.rs` carried a byte-identical copy of the old
function; it now imports the shared one.
`Quantity::scaled_value` computed `10^delta` with an unchecked `pow`,
which panicked with "attempt to multiply with overflow" on `"1e400"`
(debug) and wrapped to a nonsense value in release. Both directions now
saturate the way upstream `ScaledValue` does, and a non-zero quantity
below the target scale rounds up away from zero rather than to 0,
matching `ParseQuantity`'s `inf.RoundUp` rationale.
Tests ported from calfonso/rusternetes#87 (thanks @vyncint) — the
fractional, Pi/Ei, decimal-exponent and malformed-input cases, with
expected values checked against `resource.ParseQuantity(s).Value()`.
The `"1e400"` case asserts upstream's saturation instead of 0:
`ParseQuantity` only caps BinarySI (`maxAllowed`), so a DecimalExponent
quantity is valid and saturates in `ScaledValue`.
Consolidating the ~8 remaining bespoke quantity parsers (kubelet
eviction/runtime/translate, vpa, hpa, admission) onto `Quantity` is
tracked separately.
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #86.
parse_resource_quantityparsed the digits of a suffixed quantity withstr::parse::<i64>(), so everyresource.Quantitycarrying a decimal point silently fell back tounwrap_or(0). The upstream grammar ink8s.io/apimachinery/pkg/api/resource/quantity.goallows a decimal point with every suffix:so
"0.5Gi"is as valid as"512Mi"and denotes the same 536870912 bytes.Parsing it as
0does not fail loudly — it reads to the scheduler as "this pod requests no memory", so the pod is admitted to any node regardless of remaining capacity. The sameunwrap_or(0)applies to nodeallocatable, so a node advertising"1.5Ti"is accounted as having no memory at all and nothing schedules onto it.PiandEiwere missing from the binarySI set entirely and fell through to the same0.Verified against
resource.ParseQuantity(s).Value()fromk8s.io/[email protected]:0.5Gi1.5Gi2.5Mi1.5G1Pi1Ei1Gi128MFix
Replace the hand-rolled
strip_suffixchain with aQUANTITY_SUFFIXEStable applied over anf64parse:"1Mi"cannot be read as"1M"followed by a strayi.<decimalExponent>form are parsed directly first. Rust's float parser implements that production exactly and rejects"1E"/"1Gi", which then fall through to the table — so"129e6"stays 129000000 while"1E"still means exa.PiandEiadded to complete binarySI."inf","NaN","1e400") is rejected instead of saturating toi64::MAX, which would otherwise advertise infinite node capacity.mis the shared 10^-3 decimalSI suffix, so"100m"is 0.1 cores → 100 millicores. Result is rounded so"0.7"cores stays 700m.scheduler.rscarried a byte-identical copy of the same function. It now imports the fixed one fromadvanced.rsinstead of maintaining a second copy (net −93 lines there).Behaviour for every input the previous implementation handled correctly is unchanged; the existing
test_parse_cpu_quantityandtest_parse_memory_quantitycases pass untouched.Tests
Five new cases in
crates/scheduler/src/advanced.rs:test_parse_fractional_memory_quantity—0.5Gi,1.5Gi,0.5Mi,2.5Mi,0.5Ki,1.5G,0.5M,1.5ktest_parse_large_binary_suffixes—Ti,Pi,Eitest_parse_decimal_si_and_exponent—1T/1P/1E/1k, plus129E6and1.5e3as exponent formstest_parse_cpu_fractional_and_milli—100m,1500m,0.7,2.5,10.5mtest_parse_quantity_rejects_malformed_input—"","abc","Gi","1Xi","--5","inf","NaN","1e400"all yield 0Test plan
cargo test -p rusternetes-scheduler— 27/27 pass (was 22)cargo test --workspace— 4209 passed, 0 failed (baseline onmain: 4199 passed, 0 failed; +10 is the 5 new tests across the crate's lib and bin targets)cargo fmt --all -- --checkclean (checked over workspace members, as.github/workflows/fmt.ymldoes)cargo clippy -p rusternetes-scheduler --all-targets --all-features— no new warnings; the only hits in the touched files are the pre-existingcheck_preemption/SYSTEM_CRITICAL_PRIORITYdead-code warnings