Skip to content

liquid, limes: add support for non-standard units#84

Merged
majewsky merged 3 commits intomainfrom
liquid-nonstandard-units
Mar 12, 2026
Merged

liquid, limes: add support for non-standard units#84
majewsky merged 3 commits intomainfrom
liquid-nonstandard-units

Conversation

@majewsky
Copy link
Copy Markdown
Contributor

@majewsky majewsky commented Mar 6, 2026

Until now, only a fixed enumeration of units was supported in Limes. For example, the units "MiB", "GiB" and "TiB" were supported, but nothing inbetween those.

Compute raised a demand to us to be able to have resources with more granular units in order to express RAM quota for flavor groups. Suppose that there is a flavor group where the smallest flavor is 128 GiB and all other flavors are an integer multiple of that, quota should only be given out and commitments only be made in 128 GiB increments. Since, within LIQUID and Limes, all amounts (quota, usage, capacity, commitments) are taken to be integer multiples of the resource's defined unit, it is therefore desirable to declare this resource with a unit of "128 GiB".

I considered the option of retaining Unit with the basic definition of type Unit string, but found the idea of having to parse a Unit of e.g. "128 GiB" into a structured value for each operation to be undesirable. Therefore, Unit had to become a struct (even though that introduces its own set of pain points like having to manage the zero value).

Because the parsing and serialization logic for Unit values ended up having significant functional overlap with the existing parsing and serialization for ValueWithUnit, I created a new base type Amount to avoid code duplication. Unit now holds just an instance of type Amount, and provides its own choices for parsing and serialization on top. ValueWithUnit retains its current structure because of backwards compatibility requirements, but defers most of its work into Amount.

I had to move all those types (Amount, Unit, ValueWithUnit) into a single internal package because they need to access API within each other that I do not want to expose publicly. Specifically, ValueWithUnit needs to reach the Amount value hidden within type Unit. Amount is not an exported type and remains an implementation detail.

Unit gained some new interface implementations to cover things that were previously handled by it being a plain string:

  • encoding/json.Marshaler for JSON serialization
  • database/sql.Scanner for SQL deserialization
  • database/sql/driver.Valuer for SQL serialization

While moving things around, I also took the opportunity to remove several unused pieces from the public API:

  • YAML serialization support for Unit and ValueWithUnit (all uses of YAML for config in Limes have been replaced by JSON)
  • UnitUnspecified has (de facto) been replaced by Option[Unit]
  • FractionalValueError and IncompatibleUnitsError are not explicitly used by any callers and have been replaced by fmt.Errorf

Testing done: I have vendored this branch into both limes and limesctl, and got the tests to pass with only minimal changes:

  • Casts of the form string(unit) had to be replaced with unit.String().
  • Where Unit was used as a struct field with json:",omitempty", the linter reminded me to make those into omitzero.

@majewsky majewsky requested a review from a team as a code owner March 6, 2026 15:40
Until now, only a fixed enumeration of units was supported in Limes.
For example, the units "MiB", "GiB" and "TiB" were supported, but
nothing inbetween those.

Compute raised a demand to us to be able to have resources with more
granular units in order to express RAM quota for flavor groups. Suppose
that there is a flavor group where the smallest flavor is 128 GiB and all
other flavors are an integer multiple of that, quota should only be
given out and commitments only be made in 128 GiB increments. Since,
within LIQUID and Limes, all amounts (quota, usage, capacity,
commitments) are taken to be integer multiples of the resource's defined
unit, it is therefore desirable to declare this resource with a unit of
"128 GiB".

I considered the option of retaining Unit with the basic definition of
`type Unit string`, but found the idea of having to parse a Unit of e.g.
"128 GiB" into a structured value for each operation to be undesirable.
Therefore, Unit had to become a struct (even though that introduces its
own set of pain points like having to manage the zero value).

Because the parsing and serialization logic for Unit values ended up
having significant functional overlap with the existing parsing and
serialization for ValueWithUnit, I created a new base type Amount to
avoid code duplication. Unit now holds just an instance of type Amount,
and provides its own choices for parsing and serialization on top.
ValueWithUnit retains its current structure because of backwards
compatibility requirements, but defers most of its work into Amount.

I had to move all those types (Amount, Unit, ValueWithUnit) into a
single internal package because they need to access API within each
other that I do not want to expose publicly. Specifically,
ValueWithUnit needs to reach the Amount value hidden within type Unit.
Amount is not an exported type and remains an implementation detail.

Unit gained some new interface implementations to cover things that were
previously handled by it being a plain string:

- `encoding/json.Marshaler` for JSON serialization
- `database/sql.Scanner` for SQL deserialization
- `database/sql/driver.Valuer` for SQL serialization

While moving things around, I also took the opportunity to remove
several unused pieces from the public API:

- `UnitUnspecified` has (de facto) been replaced by `Option[Unit]`
- `FractionalValueError` and `IncompatibleUnitsError` are not explicitly
  used by any callers and have been replaced by `fmt.Errorf`

**Testing done:** I have vendored this branch into both limes and
limesctl, and got the tests to pass with only minimal changes:

- Casts of the form `string(unit)` had to be replaced with `unit.String()`.
- Where `Unit` was used as a struct field with `json:",omitempty"`,
  the linter reminded me to make those into `omitzero`.
@majewsky majewsky force-pushed the liquid-nonstandard-units branch from 836e546 to b229cf4 Compare March 6, 2026 15:41
@@ -0,0 +1,190 @@
// SPDX-FileCopyrightText: 2017-2026 SAP SE or an SAP affiliate company
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.

Just wondering, why this has a from-to copyright header? Wouldn't we have to update this periodically to the current $year? At least that's what imply from a from-to copyright statements.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I moved code that already had one of these, and updated it because a significant portion was rewritten. I'm not 100% on what the rules are for stuff like this, and didn't want to get sidetracked looking up legal advice, so just followed the existing pattern.

VoigtS
VoigtS previously requested changes Mar 9, 2026
Copy link
Copy Markdown
Member

@VoigtS VoigtS left a comment

Choose a reason for hiding this comment

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

I only have one comment.

@github-actions
Copy link
Copy Markdown

Merging this branch changes the coverage (1 decrease, 3 increase)

Impacted Packages Coverage Δ 🤖
github.com/sapcc/go-api-declarations/internal/testhelper 75.00% (-5.39%) 👎
github.com/sapcc/go-api-declarations/internal/units 73.68% (+73.68%) 🌟
github.com/sapcc/go-api-declarations/limes 100.00% (+11.11%) 🎉
github.com/sapcc/go-api-declarations/limes/rates 75.56% (ø)
github.com/sapcc/go-api-declarations/limes/resources 78.68% (ø)
github.com/sapcc/go-api-declarations/liquid 85.85% (+3.36%) 👍

Coverage by file

Changed files (no unit tests)

Changed File Coverage Δ Total Covered Missed 🤖
github.com/sapcc/go-api-declarations/internal/testhelper/testhelper.go 55.17% (-4.83%) 116 (+56) 64 (+28) 52 (+28) 👎
github.com/sapcc/go-api-declarations/internal/units/amount.go 60.00% (+60.00%) 420 (+420) 252 (+252) 168 (+168) 🌟
github.com/sapcc/go-api-declarations/internal/units/limesv1.go 93.10% (+93.10%) 174 (+174) 162 (+162) 12 (+12) 🌟
github.com/sapcc/go-api-declarations/internal/units/unit.go 85.29% (+85.29%) 204 (+204) 174 (+174) 30 (+30) 🌟
github.com/sapcc/go-api-declarations/limes/rates/metadata.go 0.00% (ø) 0 0 0
github.com/sapcc/go-api-declarations/limes/resources/commitment.go 57.14% (ø) 378 (+63) 216 (+36) 162 (+27)
github.com/sapcc/go-api-declarations/limes/resources/metadata.go 0.00% (ø) 0 0 0
github.com/sapcc/go-api-declarations/limes/unit.go 100.00% (+13.33%) 6 (-144) 6 (-124) 0 (-20) 🎉
github.com/sapcc/go-api-declarations/liquid/info.go 100.00% (ø) 96 (+16) 96 (+16) 0
github.com/sapcc/go-api-declarations/liquid/unit.go 0.00% (-42.31%) 0 (-130) 0 (-55) 0 (-75) 💀 💀 💀 💀

Please note that the "Total", "Covered", and "Missed" counts above refer to code statements instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code.

Changed unit test files

  • github.com/sapcc/go-api-declarations/internal/units/limesv1_test.go
  • github.com/sapcc/go-api-declarations/internal/units/unit_test.go
  • github.com/sapcc/go-api-declarations/limes/resources/overrides_test.go
  • github.com/sapcc/go-api-declarations/limes/unit_test.go

@majewsky majewsky dismissed VoigtS’s stale review March 12, 2026 12:11

Concern was addressed.

@majewsky majewsky merged commit 0119c70 into main Mar 12, 2026
6 checks passed
@majewsky majewsky deleted the liquid-nonstandard-units branch March 12, 2026 12:11
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.

3 participants