This commit is contained in:
mr
2026-04-27 11:16:50 +02:00
parent 0b54d6640d
commit f048b420d7
14 changed files with 484 additions and 30 deletions

View File

@@ -8,16 +8,18 @@ import (
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/booking"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/live"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/tools"
)
// InstanceCapacity holds the maximum available resources of a single resource instance.
type InstanceCapacity struct {
CPUCores map[string]float64 `json:"cpu_cores,omitempty"` // model -> total cores
GPUMemGB map[string]float64 `json:"gpu_mem_gb,omitempty"` // model -> total memory GB
RAMGB float64 `json:"ram_gb,omitempty"` // total RAM GB
StorageGB float64 `json:"storage_gb,omitempty"` // total storage GB
CPUCores map[string]float64 `json:"cpu_cores,omitempty"` // model -> total cores
GPUMemGB map[string]float64 `json:"gpu_mem_gb,omitempty"` // model -> total memory GB
RAMGB float64 `json:"ram_gb,omitempty"` // total RAM GB
StorageGB float64 `json:"storage_gb,omitempty"` // total storage GB
MaxConcurrent float64 `json:"max_concurrent,omitempty"` // HOSTED service: max simultaneous callers
}
// ResourceRequest describes the resource amounts needed for a prospective booking.
@@ -47,11 +49,14 @@ type PlannerITF interface {
}
// Planner is a volatile (non-persisted) object that organises bookings by resource.
// Only ComputeResource and StorageResource bookings appear in the schedule.
// ComputeResource, StorageResource and HOSTED ServiceResource bookings appear in the schedule.
// BlockedResources marks resources for which no matching Live was found at generation time:
// any availability check against a blocked resource returns false immediately.
type Planner struct {
GeneratedAt time.Time `json:"generated_at"`
Schedule map[string][]*PlannerSlot `json:"schedule"` // resource_id -> slots
Capacities map[string]map[string]*InstanceCapacity `json:"capacities"` // resource_id -> instance_id -> max capacity
GeneratedAt time.Time `json:"generated_at"`
Schedule map[string][]*PlannerSlot `json:"schedule"` // resource_id -> slots
Capacities map[string]map[string]*InstanceCapacity `json:"capacities"` // resource_id -> instance_id -> max capacity
BlockedResources map[string]bool `json:"blocked_resources,omitempty"` // resource_id -> no Live found
}
// Generate builds a full Planner from all active bookings.
@@ -86,9 +91,10 @@ func generate(request *tools.APIRequest, shallow bool) (*Planner, error) {
bookings := append(confirmed, drafts...)
p := &Planner{
GeneratedAt: time.Now(),
Schedule: map[string][]*PlannerSlot{},
Capacities: map[string]map[string]*InstanceCapacity{},
GeneratedAt: time.Now(),
Schedule: map[string][]*PlannerSlot{},
Capacities: map[string]map[string]*InstanceCapacity{},
BlockedResources: map[string]bool{},
}
for _, b := range bookings {
@@ -100,8 +106,10 @@ func generate(request *tools.APIRequest, shallow bool) (*Planner, error) {
continue
}
// Only compute and storage resources are eligible
if bk.ResourceType != tools.COMPUTE_RESOURCE && bk.ResourceType != tools.STORAGE_RESOURCE {
// Eligible resource types: compute, storage, and HOSTED services.
if bk.ResourceType != tools.COMPUTE_RESOURCE &&
bk.ResourceType != tools.STORAGE_RESOURCE &&
bk.ResourceType != tools.SERVICE_RESOURCE {
continue
}
@@ -111,7 +119,11 @@ func generate(request *tools.APIRequest, shallow bool) (*Planner, error) {
end = &e
}
instanceID, usage, cap := extractSlotData(bk, request)
instanceID, usage, cap, blocked := extractSlotData(bk, request)
if blocked {
p.BlockedResources[bk.ResourceID] = true
continue
}
if instanceID == "" {
instanceID = bk.InstanceID
}
@@ -151,6 +163,9 @@ func generate(request *tools.APIRequest, shallow bool) (*Planner, error) {
// Slots targeting other instances are ignored.
// If no capacity is known for this instance (never booked), it is fully available.
func (p *Planner) Check(resourceID string, instanceID string, req *ResourceRequest, start time.Time, end *time.Time) bool {
if p.BlockedResources[resourceID] {
return false // no Live found at generation time — cannot book
}
if end == nil {
e := start.Add(5 * time.Minute)
end = &e
@@ -265,6 +280,11 @@ func toPercentages(req *ResourceRequest, cap *InstanceCapacity) map[string]float
pct["storage"] = (*req.StorageGB / cap.StorageGB) * 100.0
}
// HOSTED service: each booking consumes one call slot.
if cap.MaxConcurrent > 0 {
pct["calls"] = (1.0 / cap.MaxConcurrent) * 100.0
}
return pct
}
@@ -272,9 +292,11 @@ func toPercentages(req *ResourceRequest, cap *InstanceCapacity) map[string]float
// Internal helpers
// ---------------------------------------------------------------------------
// extractSlotData parses the booking's PricedItem, loads the corresponding resource,
// and returns the instance ID, usage percentages, and instance capacity in a single pass.
func extractSlotData(bk *booking.Booking, request *tools.APIRequest) (instanceID string, usage map[string]float64, cap *InstanceCapacity) {
// extractSlotData parses the booking's PricedItem, loads the corresponding Live resource
// as the authoritative capacity source, and returns the instance ID, usage percentages,
// capacity, and whether a matching Live was found.
// blocked=true means no Live exists for this resource; the resource must not be scheduled.
func extractSlotData(bk *booking.Booking, request *tools.APIRequest) (instanceID string, usage map[string]float64, cap *InstanceCapacity, blocked bool) {
usage = map[string]float64{}
if len(bk.PricedItem) == 0 {
return
@@ -289,6 +311,8 @@ func extractSlotData(bk *booking.Booking, request *tools.APIRequest) (instanceID
instanceID, usage, cap = extractComputeSlot(b, bk.ResourceID, request)
case tools.STORAGE_RESOURCE:
instanceID, usage, cap = extractStorageSlot(b, bk.ResourceID, request)
case tools.SERVICE_RESOURCE:
instanceID, usage, cap, blocked = extractServiceSlot(b, bk.ResourceID, request)
}
return
}
@@ -381,6 +405,51 @@ func extractStorageSlot(pricedJSON []byte, resourceID string, request *tools.API
return
}
// extractServiceSlot extracts the instance ID, usage, and capacity for a HOSTED service booking.
// The LiveService is the authoritative source for MaxConcurrent — the ServiceResource is not trusted.
// If no LiveService references this resourceID, blocked=true signals the resource cannot be scheduled.
func extractServiceSlot(pricedJSON []byte, resourceID string, request *tools.APIRequest) (instanceID string, usage map[string]float64, cap *InstanceCapacity, blocked bool) {
usage = map[string]float64{}
var priced resources.PricedServiceResource
if err := json.Unmarshal(pricedJSON, &priced); err != nil {
blocked = true
return
}
// LiveService is the authoritative capacity source — look it up by resources_id.
liveResults, _, err := (&live.LiveService{}).GetAccessor(request).Search(
&dbs.Filters{
And: map[string][]dbs.Filter{
"resources_id": {{Operator: dbs.EQUAL.String(), Value: resourceID}},
},
}, "*", false, 0, 1)
if err != nil || len(liveResults) == 0 {
blocked = true // no Live → cannot schedule
return
}
ls := liveResults[0].(*live.LiveService)
if ls.MaxConcurrent <= 0 {
blocked = true
return
}
// Instance ID: use the first instance referenced by the priced item.
instanceID = priced.GetID()
if instanceID == "" {
instanceID = resourceID // fallback: treat the resource itself as the instance key
}
maxC := float64(ls.MaxConcurrent)
cap = &InstanceCapacity{
CPUCores: map[string]float64{},
GPUMemGB: map[string]float64{},
MaxConcurrent: maxC,
}
usage["calls"] = (1.0 / maxC) * 100.0
return
}
// findComputeInstance returns the instance referenced by the priced item's InstancesRefs,
// falling back to the first available instance.
func findComputeInstance(compute *resources.ComputeResource, refs map[string]string) *resources.ComputeResourceInstance {