Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 56 additions & 4 deletions internal/knowledge/extractor/plugins/compute/flavor_groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
_ "embed"
"encoding/json"
"errors"
"fmt"
"sort"

"github.com/cobaltcore-dev/cortex/internal/knowledge/extractor/plugins"
Expand Down Expand Up @@ -46,9 +47,55 @@ type FlavorGroupFeature struct {
RamCoreRatioMax *uint64 `json:"ramCoreRatioMax,omitempty"`
}

// HasFixedRamCoreRatio returns true if all flavors in this group have the same RAM/core ratio.
func (f *FlavorGroupFeature) HasFixedRamCoreRatio() bool {
return f.RamCoreRatio != nil
if f.RamCoreRatio == nil {
return false
}
if f.RamCoreRatioMin == nil && f.RamCoreRatioMax == nil {
return true
}
return f.RamCoreRatioMin != nil && f.RamCoreRatioMax != nil &&
*f.RamCoreRatio == *f.RamCoreRatioMin && *f.RamCoreRatio == *f.RamCoreRatioMax
}

func (f *FlavorGroupFeature) Validate() error {
hasRatio := f.RamCoreRatio != nil
hasMin := f.RamCoreRatioMin != nil
hasMax := f.RamCoreRatioMax != nil

allThreeSame := hasRatio && hasMin && hasMax &&
*f.RamCoreRatio == *f.RamCoreRatioMin && *f.RamCoreRatio == *f.RamCoreRatioMax
isFixed := (hasRatio && !hasMin && !hasMax) || allThreeSame
isVariable := !hasRatio && hasMin && hasMax
isNone := !hasRatio && !hasMin && !hasMax

if !isFixed && !isVariable && !isNone {
return fmt.Errorf("flavor group %q has inconsistent ratio fields", f.Name)
}
if isVariable && *f.RamCoreRatioMin >= *f.RamCoreRatioMax {
return fmt.Errorf("flavor group %q: RamCoreRatioMin (%d) must be less than RamCoreRatioMax (%d)", f.Name, *f.RamCoreRatioMin, *f.RamCoreRatioMax)
}
if (isFixed || isVariable) && f.SmallestFlavor.MemoryMB == 0 {
return fmt.Errorf("flavor group %q: SmallestFlavor.MemoryMB must be non-zero", f.Name)
}
return nil
}

// RAMUnitMiB returns MiB per one declared LIQUID RAM unit:
// fixed-ratio groups use slots (SmallestFlavor.MemoryMB MiB each); variable-ratio use GiB (1024 MiB).
func (f *FlavorGroupFeature) RAMUnitMiB() uint64 {
if f.HasFixedRamCoreRatio() && f.SmallestFlavor.MemoryMB > 0 {
return f.SmallestFlavor.MemoryMB
}
return 1024
}

func (f *FlavorGroupFeature) DeclaredUnitsToGiB(units int64) int64 {
return units * int64(f.RAMUnitMiB()) / 1024 //nolint:gosec
}

func (f *FlavorGroupFeature) GiBToDeclaredUnits(gib int64) int64 {
return gib * 1024 / int64(f.RAMUnitMiB()) //nolint:gosec
}

// flavorRow represents a row from the SQL query.
Expand Down Expand Up @@ -177,15 +224,20 @@ func (e *FlavorGroupExtractor) Extract() ([]plugins.Feature, error) {
"ramCoreRatioMin", ramCoreRatioMin,
"ramCoreRatioMax", ramCoreRatioMax)

features = append(features, FlavorGroupFeature{
fg := FlavorGroupFeature{
Name: groupName,
Flavors: flavors,
LargestFlavor: largest,
SmallestFlavor: smallest,
RamCoreRatio: ramCoreRatio,
RamCoreRatioMin: ramCoreRatioMin,
RamCoreRatioMax: ramCoreRatioMax,
})
}
if err := fg.Validate(); err != nil {
flavorGroupLog.Error(err, "skipping flavor group with invalid data", "groupName", groupName)
continue
}
features = append(features, fg)
}

// Sort features by group name for consistent ordering
Expand Down
211 changes: 211 additions & 0 deletions internal/knowledge/extractor/plugins/compute/flavor_groups_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,3 +389,214 @@ func TestFlavorGroupExtractor_RamCoreRatio_FixedRatio(t *testing.T) {
t.Errorf("expected RamCoreRatioMax=nil for fixed ratio, got %d", *fg.RamCoreRatioMax)
}
}

func TestFlavorGroupFeature_Validate(t *testing.T) {
ratio := uint64(4096)
lo, hi := uint64(2048), uint64(8192)
tests := []struct {
name string
fg FlavorGroupFeature
wantErr bool
}{
{
name: "valid: all nil (no ratio info)",
fg: FlavorGroupFeature{Name: "none"},
wantErr: false,
},
{
name: "valid: fixed — only RamCoreRatio set",
fg: FlavorGroupFeature{
Name: "fixed",
RamCoreRatio: &ratio,
SmallestFlavor: FlavorInGroup{MemoryMB: 8192},
},
wantErr: false,
},
{
name: "valid: fixed — all three set to same value",
fg: FlavorGroupFeature{
Name: "fixed-all-same",
RamCoreRatio: &ratio,
RamCoreRatioMin: &ratio,
RamCoreRatioMax: &ratio,
SmallestFlavor: FlavorInGroup{MemoryMB: 8192},
},
wantErr: false,
},
{
name: "valid: variable — Min < Max, SmallestFlavor set",
fg: FlavorGroupFeature{
Name: "variable",
RamCoreRatioMin: &lo,
RamCoreRatioMax: &hi,
SmallestFlavor: FlavorInGroup{MemoryMB: 8192},
},
wantErr: false,
},
{
name: "invalid: RamCoreRatio + Min/Max set with different values",
fg: FlavorGroupFeature{
Name: "inconsistent",
RamCoreRatio: &ratio,
RamCoreRatioMin: &lo,
RamCoreRatioMax: &hi,
SmallestFlavor: FlavorInGroup{MemoryMB: 8192},
},
wantErr: true,
},
{
name: "invalid: only RamCoreRatioMin set",
fg: FlavorGroupFeature{Name: "partial", RamCoreRatioMin: &ratio},
wantErr: true,
},
{
name: "invalid: only RamCoreRatioMax set",
fg: FlavorGroupFeature{Name: "partial", RamCoreRatioMax: &ratio},
wantErr: true,
},
{
name: "invalid: variable with Min > Max",
fg: FlavorGroupFeature{
Name: "inverted",
RamCoreRatioMin: &hi,
RamCoreRatioMax: &lo,
SmallestFlavor: FlavorInGroup{MemoryMB: 8192},
},
wantErr: true,
},
{
name: "invalid: variable with Min == Max (should be fixed)",
fg: FlavorGroupFeature{
Name: "equal-range",
RamCoreRatioMin: &ratio,
RamCoreRatioMax: &ratio,
SmallestFlavor: FlavorInGroup{MemoryMB: 8192},
},
wantErr: true,
},
{
name: "invalid: fixed with SmallestFlavor.MemoryMB == 0",
fg: FlavorGroupFeature{
Name: "fixed-no-smallest",
RamCoreRatio: &ratio,
},
wantErr: true,
},
{
name: "invalid: variable with SmallestFlavor.MemoryMB == 0",
fg: FlavorGroupFeature{
Name: "variable-no-smallest",
RamCoreRatioMin: &lo,
RamCoreRatioMax: &hi,
},
wantErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.fg.Validate()
if (err != nil) != tc.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, tc.wantErr)
}
})
}
}

func TestFlavorGroupFeature_RAMUnitMiB(t *testing.T) {
ratio := uint64(4096)
tests := []struct {
name string
fg FlavorGroupFeature
want uint64
}{
{
name: "fixed-ratio returns SmallestFlavor.MemoryMB",
fg: FlavorGroupFeature{
RamCoreRatio: &ratio,
SmallestFlavor: FlavorInGroup{MemoryMB: 2048},
},
want: 2048,
},
{
name: "variable-ratio returns 1024",
fg: FlavorGroupFeature{
RamCoreRatio: nil,
},
want: 1024,
},
{
name: "fixed-ratio (all three same) returns SmallestFlavor.MemoryMB",
fg: FlavorGroupFeature{
RamCoreRatio: &ratio,
RamCoreRatioMin: &ratio,
RamCoreRatioMax: &ratio,
SmallestFlavor: FlavorInGroup{MemoryMB: 2048},
},
want: 2048,
},
{
name: "RamCoreRatio set but MemoryMB zero falls back to 1024 (invalid data, safe fallback)",
fg: FlavorGroupFeature{
RamCoreRatio: &ratio,
SmallestFlavor: FlavorInGroup{MemoryMB: 0},
},
want: 1024,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := tc.fg.RAMUnitMiB(); got != tc.want {
t.Errorf("RAMUnitMiB() = %d, want %d", got, tc.want)
}
})
}
}

func TestFlavorGroupFeature_UnitConversions(t *testing.T) {
ratio := uint64(4096)
tests := []struct {
name string
fg FlavorGroupFeature
units int64
expectedGiB int64
giB int64
expectedUnits int64
}{
{
name: "fixed-ratio 2 GiB/slot: 5 slots → 10 GiB, 10 GiB → 5 slots",
fg: FlavorGroupFeature{
RamCoreRatio: &ratio,
SmallestFlavor: FlavorInGroup{MemoryMB: 2048},
},
units: 5, expectedGiB: 10,
giB: 10, expectedUnits: 5,
},
{
name: "variable-ratio (1 GiB/unit): 50 units → 50 GiB, 50 GiB → 50 units",
fg: FlavorGroupFeature{
RamCoreRatio: nil,
},
units: 50, expectedGiB: 50,
giB: 50, expectedUnits: 50,
},
{
name: "fixed-ratio 1 GiB/slot (1024 MiB): conversion is a no-op",
fg: FlavorGroupFeature{
RamCoreRatio: &ratio,
SmallestFlavor: FlavorInGroup{MemoryMB: 1024},
},
units: 100, expectedGiB: 100,
giB: 100, expectedUnits: 100,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := tc.fg.DeclaredUnitsToGiB(tc.units); got != tc.expectedGiB {
t.Errorf("DeclaredUnitsToGiB(%d) = %d, want %d", tc.units, got, tc.expectedGiB)
}
if got := tc.fg.GiBToDeclaredUnits(tc.giB); got != tc.expectedUnits {
t.Errorf("GiBToDeclaredUnits(%d) = %d, want %d", tc.giB, got, tc.expectedUnits)
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ ProcessLoop:
break ProcessLoop
}

groupData := flavorGroups[flavorGroupName]
ramUnitMiB := groupData.RAMUnitMiB()

groupResourceConf := api.config.ResourceConfigForGroup(flavorGroupName)
var handlesCommitments bool
switch resourceType {
Expand Down Expand Up @@ -263,7 +266,7 @@ ProcessLoop:
}

stateDesired, err := commitments.FromChangeCommitmentTargetState(
commitment, string(projectID), domainID, flavorGroupName, resourceType, string(req.AZ))
commitment, string(projectID), domainID, flavorGroupName, resourceType, string(req.AZ), ramUnitMiB)
if err != nil {
failedReason = fmt.Sprintf("commitment %s: %s", commitment.UUID, err)
rollback = true
Expand Down
17 changes: 12 additions & 5 deletions internal/scheduling/reservations/commitments/api/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,19 @@ func (api *HTTPAPI) buildServiceInfo(ctx context.Context, logger logr.Logger) (l
if resCfg.RAM.HandlesCommitments {
ramTopology = liquid.AZSeparatedTopology
}
// Fixed-ratio groups: unit is 1 slot (= 1 smallest-flavor instance); variable-ratio: GiB.
var ramUnit liquid.Unit
var ramDisplayName string
if groupData.HasFixedRamCoreRatio() {
ramUnit = liquid.UnitNone
ramDisplayName = fmt.Sprintf("multiples of %d MiB (usable by: %s)", groupData.SmallestFlavor.MemoryMB, flavorListStr)
} else {
ramUnit = liquid.UnitGibibytes
ramDisplayName = fmt.Sprintf("GiB of RAM (usable by: %s)", flavorListStr)
}
resources[ramResourceName] = liquid.ResourceInfo{
DisplayName: fmt.Sprintf(
"GiB of RAM (usable by: %s)",
flavorListStr,
),
Unit: liquid.UnitGibibytes,
DisplayName: ramDisplayName,
Unit: ramUnit,
Topology: ramTopology,
NeedsResourceDemand: false,
HasCapacity: resCfg.RAM.HasCapacity,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,14 @@ func TestHandleInfo_ResourceFlagsFromConfig(t *testing.T) {
checkAttrsRatio(t, "hw_version_hana_fixed_ram", ramResource.Attributes, 4, nil, nil)
// v2_variable has ramCoreRatioMin=2048 MiB/vCPU, ramCoreRatioMax=16384 MiB/vCPU → expect 2, 16 GiB/vCPU.
checkAttrsRatio(t, "hw_version_v2_variable_ram", v2RamResource.Attributes, 0, ptr(uint64(2)), ptr(uint64(16)))

// Verify RAM units: fixed-ratio groups use UnitNone (slot-based), variable-ratio use UnitGibibytes.
if ramResource.Unit != liquid.UnitNone {
t.Errorf("hw_version_hana_fixed_ram: expected Unit=%q (slot-based), got %q", liquid.UnitNone, ramResource.Unit)
}
if v2RamResource.Unit != liquid.UnitGibibytes {
t.Errorf("hw_version_v2_variable_ram: expected Unit=%q, got %q", liquid.UnitGibibytes, v2RamResource.Unit)
}
}

func TestHandleInfo_TopologyFollowsHandlesCommitments(t *testing.T) {
Expand Down
Loading