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
32 changes: 10 additions & 22 deletions internal/scheduling/reservations/commitments/api/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func (api *HTTPAPI) recordInfoMetrics(statusCode int, startTime time.Time) {
}

// resourceAttributes holds the custom attributes for a resource in the info API response.
// Ratio values are in GiB per vCPU, matching the RAM resource unit (UnitGibibytes).
// Ratio values are in GiB per vCPU.
type resourceAttributes struct {
RamCoreRatio *uint64 `json:"ramCoreRatio,omitempty"`
RamCoreRatioMin *uint64 `json:"ramCoreRatioMin,omitempty"`
Expand Down Expand Up @@ -137,26 +137,22 @@ func (api *HTTPAPI) buildServiceInfo(ctx context.Context, logger logr.Logger) (l

// === 1. RAM Resource ===
ramResourceName := liquid.ResourceName(commitments.ResourceNameRAM(groupName))
// Determine topology: AZSeparatedTopology only for groups that accept commitments
// (AZSeparatedTopology means quota is also AZ-aware, required when HasQuota=true)
ramTopology := liquid.AZAwareTopology
if resCfg.RAM.HandlesCommitments {
ramTopology = liquid.AZSeparatedTopology
// Fixed-ratio groups: unit = smallest flavor's RAM in MiB (e.g. "480 GiB" for hana);
// variable-ratio groups: unit = 1 GiB. RAMUnitMiB() encodes both cases.
ramUnit, err := liquid.UnitMebibytes.MultiplyBy(groupData.RAMUnitMiB())
if err != nil {
return liquid.ServiceInfo{}, fmt.Errorf("failed to create RAM unit for flavor group %q: %w", groupName, err)
}
// 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
if groupData.HasFixedRamCoreRatio() && groupData.SmallestFlavor.MemoryMB > 0 {
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: ramDisplayName,
Unit: ramUnit,
Topology: ramTopology,
Topology: liquid.AZSeparatedTopology,
NeedsResourceDemand: false,
HasCapacity: resCfg.RAM.HasCapacity,
HasQuota: resCfg.RAM.HasQuota,
Expand All @@ -166,17 +162,13 @@ func (api *HTTPAPI) buildServiceInfo(ctx context.Context, logger logr.Logger) (l

// === 2. Cores Resource ===
coresResourceName := liquid.ResourceName(commitments.ResourceNameCores(groupName))
coresTopology := liquid.AZAwareTopology
if resCfg.Cores.HandlesCommitments {
coresTopology = liquid.AZSeparatedTopology
}
resources[coresResourceName] = liquid.ResourceInfo{
DisplayName: fmt.Sprintf(
"CPU cores (usable by: %s)",
flavorListStr,
),
Unit: liquid.UnitNone,
Topology: coresTopology,
Topology: liquid.AZSeparatedTopology,
NeedsResourceDemand: false,
HasCapacity: resCfg.Cores.HasCapacity,
HasQuota: resCfg.Cores.HasQuota,
Expand All @@ -186,17 +178,13 @@ func (api *HTTPAPI) buildServiceInfo(ctx context.Context, logger logr.Logger) (l

// === 3. Instances Resource ===
instancesResourceName := liquid.ResourceName(commitments.ResourceNameInstances(groupName))
instancesTopology := liquid.AZAwareTopology
if resCfg.Instances.HandlesCommitments {
instancesTopology = liquid.AZSeparatedTopology
}
resources[instancesResourceName] = liquid.ResourceInfo{
DisplayName: fmt.Sprintf(
"instances (usable by: %s)",
flavorListStr,
),
Unit: liquid.UnitNone,
Topology: instancesTopology,
Topology: liquid.AZSeparatedTopology,
NeedsResourceDemand: false,
HasCapacity: resCfg.Instances.HasCapacity,
HasQuota: resCfg.Instances.HasQuota,
Expand Down
39 changes: 22 additions & 17 deletions internal/scheduling/reservations/commitments/api/info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ func TestHandleInfo_ResourceFlagsFromConfig(t *testing.T) {
t.Error("hw_version_hana_fixed_ram: expected HasQuota=true (fixed ratio groups accept quotas)")
}

// Test Cores resource: hw_version_hana_fixed_cores (HandlesCommitments=false → AZAwareTopology)
// Test Cores resource: hw_version_hana_fixed_cores
coresResource, ok := serviceInfo.Resources["hw_version_hana_fixed_cores"]
if !ok {
t.Fatal("expected hw_version_hana_fixed_cores resource to exist")
Expand All @@ -258,14 +258,14 @@ func TestHandleInfo_ResourceFlagsFromConfig(t *testing.T) {
if coresResource.HandlesCommitments {
t.Error("hw_version_hana_fixed_cores: expected HandlesCommitments=false")
}
if coresResource.Topology != liquid.AZAwareTopology {
t.Errorf("hw_version_hana_fixed_cores: expected Topology=%q, got %q", liquid.AZAwareTopology, coresResource.Topology)
if coresResource.Topology != liquid.AZSeparatedTopology {
t.Errorf("hw_version_hana_fixed_cores: expected Topology=%q, got %q", liquid.AZSeparatedTopology, coresResource.Topology)
}
if coresResource.HasQuota {
t.Error("hw_version_hana_fixed_cores: expected HasQuota=false")
}

// Test Instances resource: hw_version_hana_fixed_instances (HandlesCommitments=false → AZAwareTopology)
// Test Instances resource: hw_version_hana_fixed_instances
instancesResource, ok := serviceInfo.Resources["hw_version_hana_fixed_instances"]
if !ok {
t.Fatal("expected hw_version_hana_fixed_instances resource to exist")
Expand All @@ -276,8 +276,8 @@ func TestHandleInfo_ResourceFlagsFromConfig(t *testing.T) {
if instancesResource.HandlesCommitments {
t.Error("hw_version_hana_fixed_instances: expected HandlesCommitments=false")
}
if instancesResource.Topology != liquid.AZAwareTopology {
t.Errorf("hw_version_hana_fixed_instances: expected Topology=%q, got %q", liquid.AZAwareTopology, instancesResource.Topology)
if instancesResource.Topology != liquid.AZSeparatedTopology {
t.Errorf("hw_version_hana_fixed_instances: expected Topology=%q, got %q", liquid.AZSeparatedTopology, instancesResource.Topology)
}
if instancesResource.HasQuota {
t.Error("hw_version_hana_fixed_instances: expected HasQuota=false")
Expand All @@ -294,8 +294,8 @@ func TestHandleInfo_ResourceFlagsFromConfig(t *testing.T) {
if v2RamResource.HandlesCommitments {
t.Error("hw_version_v2_variable_ram: expected HandlesCommitments=false (not in config)")
}
if v2RamResource.Topology != liquid.AZAwareTopology {
t.Errorf("hw_version_v2_variable_ram: expected Topology=%q, got %q", liquid.AZAwareTopology, v2RamResource.Topology)
if v2RamResource.Topology != liquid.AZSeparatedTopology {
t.Errorf("hw_version_v2_variable_ram: expected Topology=%q, got %q", liquid.AZSeparatedTopology, v2RamResource.Topology)
}
if v2RamResource.HasQuota {
t.Error("hw_version_v2_variable_ram: expected HasQuota=false (variable ratio)")
Expand All @@ -311,8 +311,8 @@ func TestHandleInfo_ResourceFlagsFromConfig(t *testing.T) {
if v2CoresResource.HandlesCommitments {
t.Error("hw_version_v2_variable_cores: expected HandlesCommitments=false")
}
if v2CoresResource.Topology != liquid.AZAwareTopology {
t.Errorf("hw_version_v2_variable_cores: expected Topology=%q, got %q", liquid.AZAwareTopology, v2CoresResource.Topology)
if v2CoresResource.Topology != liquid.AZSeparatedTopology {
t.Errorf("hw_version_v2_variable_cores: expected Topology=%q, got %q", liquid.AZSeparatedTopology, v2CoresResource.Topology)
}
if v2CoresResource.HasQuota {
t.Error("hw_version_v2_variable_cores: expected HasQuota=false")
Expand All @@ -328,8 +328,8 @@ func TestHandleInfo_ResourceFlagsFromConfig(t *testing.T) {
if v2InstancesResource.HandlesCommitments {
t.Error("hw_version_v2_variable_instances: expected HandlesCommitments=false")
}
if v2InstancesResource.Topology != liquid.AZAwareTopology {
t.Errorf("hw_version_v2_variable_instances: expected Topology=%q, got %q", liquid.AZAwareTopology, v2InstancesResource.Topology)
if v2InstancesResource.Topology != liquid.AZSeparatedTopology {
t.Errorf("hw_version_v2_variable_instances: expected Topology=%q, got %q", liquid.AZSeparatedTopology, v2InstancesResource.Topology)
}
if v2InstancesResource.HasQuota {
t.Error("hw_version_v2_variable_instances: expected HasQuota=false")
Expand All @@ -341,17 +341,22 @@ func TestHandleInfo_ResourceFlagsFromConfig(t *testing.T) {
// 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)
// Verify RAM units: fixed-ratio groups use smallest-flavor-based unit (e.g. "16 GiB"),
// variable-ratio groups use UnitGibibytes ("GiB").
expectedFixedUnit, err := liquid.UnitMebibytes.MultiplyBy(16384) // hana_fixed smallest flavor: 16384 MiB = 16 GiB
if err != nil {
t.Fatalf("failed to create expected fixed unit: %v", err)
}
if ramResource.Unit != expectedFixedUnit {
t.Errorf("hw_version_hana_fixed_ram: expected Unit=%q (slot-based), got %q", expectedFixedUnit, 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) {
// Verifies that any resource with HandlesCommitments=true gets AZSeparatedTopology,
func TestHandleInfo_AllResourcesAZSeparated(t *testing.T) {
// Verifies that all resources always get AZSeparatedTopology.
// regardless of whether it's RAM, Cores, or Instances.
scheme := runtime.NewScheme()
if err := v1alpha1.AddToScheme(scheme); err != nil {
Expand Down
47 changes: 29 additions & 18 deletions internal/scheduling/reservations/commitments/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -509,29 +509,34 @@ func (c *UsageCalculator) buildUsageResponse(
// Build ResourceUsageReport for all flavor groups (not just those with fixed ratio)
for flavorGroupName := range flavorGroups {
// All flavor groups are included in usage reporting.
resCfg := config.ResourceConfigForGroup(flavorGroupName)

// helper: look up stored quota for a resource in a given AZ (stored in GiB).
// Returns -1 (infinite) if not found. Unit conversion is done by the caller.
lookupQuotaGiB := func(resourceName string, az liquid.AvailabilityZone) int64 {
if quotaByResourceAZ == nil {
return -1
}
if azMap, ok := quotaByResourceAZ[resourceName]; ok {
if q, ok := azMap[string(az)]; ok {
return q
}
}
return -1
}

// === 1. RAM Resource ===
ramResourceName := liquid.ResourceName(ResourceNameRAM(flavorGroupName))
ramPerAZ := make(map[liquid.AvailabilityZone]*liquid.AZResourceUsageReport)
// Include per-AZ quota for AZSeparatedTopology resources — same condition as info.go.
ramHasAZQuota := config.ResourceConfigForGroup(flavorGroupName).RAM.HandlesCommitments
for _, az := range allAZs {
report := &liquid.AZResourceUsageReport{
Usage: 0,
Subresources: []liquid.Subresource{},
}
if ramHasAZQuota {
quota := int64(-1) // default: infinite
if quotaByResourceAZ != nil {
if azMap, ok := quotaByResourceAZ[string(ramResourceName)]; ok {
if q, ok := azMap[string(az)]; ok {
// CRD stores quota in GiB; convert to declared unit for Limes.
fg := flavorGroups[flavorGroupName]
quota = fg.GiBToDeclaredUnits(q)
}
}
}
report.Quota = Some(quota)
if resCfg.RAM.HasQuota {
// CRD stores quota in GiB; convert to declared unit for Limes.
fg := flavorGroups[flavorGroupName]
report.Quota = Some(fg.GiBToDeclaredUnits(lookupQuotaGiB(string(ramResourceName), az)))
}
ramPerAZ[az] = report
}
Expand All @@ -541,7 +546,6 @@ func (c *UsageCalculator) buildUsageResponse(
continue // skip VMs in AZs not in allAZs
}
ramPerAZ[az].Usage = data.ramUsage
// Subresources are only on instances resource
}
}
resources[ramResourceName] = &liquid.ResourceUsageReport{
Expand All @@ -552,18 +556,21 @@ func (c *UsageCalculator) buildUsageResponse(
coresResourceName := liquid.ResourceName(ResourceNameCores(flavorGroupName))
coresPerAZ := make(map[liquid.AvailabilityZone]*liquid.AZResourceUsageReport)
for _, az := range allAZs {
coresPerAZ[az] = &liquid.AZResourceUsageReport{
report := &liquid.AZResourceUsageReport{
Usage: 0,
Subresources: []liquid.Subresource{},
}
if resCfg.Cores.HasQuota {
report.Quota = Some(lookupQuotaGiB(string(coresResourceName), az))
}
coresPerAZ[az] = report
}
if azData, exists := usageByFlavorGroupAZ[flavorGroupName]; exists {
for az, data := range azData {
if _, known := coresPerAZ[az]; !known {
continue // skip VMs in AZs not in allAZs
}
coresPerAZ[az].Usage = data.coresUsage
// Subresources are only on instances resource
}
}
resources[coresResourceName] = &liquid.ResourceUsageReport{
Expand All @@ -574,10 +581,14 @@ func (c *UsageCalculator) buildUsageResponse(
instancesResourceName := liquid.ResourceName(ResourceNameInstances(flavorGroupName))
instancesPerAZ := make(map[liquid.AvailabilityZone]*liquid.AZResourceUsageReport)
for _, az := range allAZs {
instancesPerAZ[az] = &liquid.AZResourceUsageReport{
report := &liquid.AZResourceUsageReport{
Usage: 0,
Subresources: []liquid.Subresource{},
}
if resCfg.Instances.HasQuota {
report.Quota = Some(lookupQuotaGiB(string(instancesResourceName), az))
}
instancesPerAZ[az] = report
}
if azData, exists := usageByFlavorGroupAZ[flavorGroupName]; exists {
for az, data := range azData {
Expand Down
Loading