Skip to content

Commit e284ce4

Browse files
committed
prototype of custom resource scheduling
Platforms other than Linux, especially non-Unix platforms, may have a different view of resources that don't model well as `resources.cpu` or `resources.memory`. And users with unusual deployment environments have asked about the possibility of scheduling resources specific to those environments. This PR is a prototype of the kind of interface we might want to be able to support. Nodes get a `client.custom_resource` block where they can define a resource the node will make available in its fingerprint. Resources are one of five types, designed to make it possible to model all our existing resources as custom resources: * `ratio`: The resource is used as a value relative to other tasks with the same resource. An example of this would be Linux `cpu.weight` as implemented by a systemd unit file. * `capped-ratio`: Like a ratio, except all the quantity used by tasks has a maximum total value. An example of this would be Linux `cpu.weight` as currently implemented in Nomad, where the "cap" is derived from the total Mhz of CPUs on the host. * `countable`: The resource has a fixed but fungible amount. An example of this would be memory allocation (ignoring NUMA), where there's a certain amount of memory available on the host and it's "used up" by allocations, but we don't care about identity of the individual blocks of memory. * `dynamic`: The resource has a fixed set of items with exclusive access, but the job doesn't care which ones it gets. An example of this would be Nomad's current dynamic port assignment or `resource.cores`. * `static`: The resource has a fixed set of items with exclusive access, and the job wants a specific one of those items. An example of this woul dbe Nomad's current static port assignment. We can use this prototype to anchor discussions about how we might implement a set of custom resource scheduling features but it's nowhere near production-ready. I've included enough plumbing to implement these five resource types, fingerprint them on clients via config files, and schedule them for allocations. What's not included, all of which we'd want to solve in any productionized version of this work: * Support for exposing the custom resource allocation to a task driver. We'd need to thread custom resources into the protobufs we send via `go-plugin` and then the user's task driver would need to consume that data. * Support for preemption * Support for quotas * Support for dynamically adding custom resources to a node Ref: #1081
1 parent 8419ccd commit e284ce4

File tree

17 files changed

+1117
-3
lines changed

17 files changed

+1117
-3
lines changed

api/allocations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,12 +436,14 @@ type AllocatedTaskResources struct {
436436
Memory AllocatedMemoryResources
437437
Networks []*NetworkResource
438438
Devices []*AllocatedDeviceResource
439+
Custom []*CustomResource
439440
}
440441

441442
type AllocatedSharedResources struct {
442443
DiskMB int64
443444
Networks []*NetworkResource
444445
Ports []PortMapping
446+
Custom []*CustomResource
445447
}
446448

447449
type PortMapping struct {

api/nodes.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ type NodeResources struct {
587587
Disk NodeDiskResources
588588
Networks []*NetworkResource
589589
Devices []*NodeDeviceResource
590+
Custom []*NodeCustomResource
590591

591592
MinDynamicPort int
592593
MaxDynamicPort int
@@ -629,6 +630,18 @@ type NodeReservedNetworkResources struct {
629630
ReservedHostPorts string
630631
}
631632

633+
type NodeCustomResource struct {
634+
Name string `hcl:"name,label"`
635+
Version uint64 `hcl:"version,optional"`
636+
Type CustomResourceType `hcl:"type,optional"`
637+
Scope CustomResourceScope `hcl:"scope,optional"`
638+
639+
Quantity int64 `hcl:"quantity,optional"`
640+
Range string `hcl:"range,optional"`
641+
Items []any `hcl:"items,optional"`
642+
Meta map[string]string `hcl:"meta,block"`
643+
}
644+
632645
type CSITopologyRequest struct {
633646
Required []*CSITopology `hcl:"required"`
634647
Preferred []*CSITopology `hcl:"preferred"`

api/resources.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type Resources struct {
2020
Devices []*RequestedDevice `hcl:"device,block"`
2121
NUMA *NUMAResource `hcl:"numa,block"`
2222
SecretsMB *int `mapstructure:"secrets" hcl:"secrets,optional"`
23+
Custom []*CustomResource `mapstructure:"custom" hcl:"custom,block"`
2324

2425
// COMPAT(0.10)
2526
// XXX Deprecated. Please do not use. The field will be removed in Nomad
@@ -330,3 +331,32 @@ func (d *RequestedDevice) Canonicalize() {
330331
a.Canonicalize()
331332
}
332333
}
334+
335+
type CustomResource struct {
336+
Name string `hcl:"name,label"`
337+
Version uint64 `hcl:"version,optional"`
338+
Type CustomResourceType `hcl:"type,optional"`
339+
Scope CustomResourceScope `hcl:"scope,optional"`
340+
341+
Quantity int64 `hcl:"quantity,optional"`
342+
Range string `hcl:"range,optional"`
343+
Items []any `hcl:"items,optional"`
344+
Constraints []*Constraint `hcl:"constraint,block"`
345+
}
346+
347+
type CustomResourceScope string
348+
349+
const (
350+
CustomResourceScopeGroup CustomResourceScope = "group"
351+
CustomResourceScopeTask CustomResourceScope = "task"
352+
)
353+
354+
type CustomResourceType string
355+
356+
const (
357+
CustomResourceTypeRatio CustomResourceType = "ratio" // ex. weight
358+
CustomResourceTypeCappedRatio CustomResourceType = "capped-ratio" // ex. resource.cpu
359+
CustomResourceTypeCountable CustomResourceType = "countable" // ex. memory, disk
360+
CustomResourceTypeDynamicInstance CustomResourceType = "dynamic" // ex. ports, cores
361+
CustomResourceTypeStaticInstance CustomResourceType = "static" // ex. ports, devices
362+
)

client/client.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1649,12 +1649,14 @@ func (c *Client) setupNode() error {
16491649
node.NodeResources.MinDynamicPort = newConfig.MinDynamicPort
16501650
node.NodeResources.MaxDynamicPort = newConfig.MaxDynamicPort
16511651
node.NodeResources.Processors = newConfig.Node.NodeResources.Processors
1652+
node.NodeResources.Custom = newConfig.CustomResources
16521653

16531654
if node.NodeResources.Processors.Empty() {
16541655
node.NodeResources.Processors = structs.NodeProcessorResources{
16551656
Topology: &numalib.Topology{},
16561657
}
16571658
}
1659+
node.NodeResources.Custom = newConfig.CustomResources
16581660
}
16591661
if node.ReservedResources == nil {
16601662
node.ReservedResources = &structs.NodeReservedResources{}
@@ -1813,6 +1815,8 @@ func (c *Client) updateNodeFromFingerprint(response *fingerprint.FingerprintResp
18131815
if cpu := response.NodeResources.Processors.TotalCompute(); cpu > 0 {
18141816
newConfig.CpuCompute = cpu
18151817
}
1818+
1819+
response.NodeResources.Custom = newConfig.CustomResources
18161820
}
18171821

18181822
if nodeHasChanged {

client/config/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,8 @@ type Config struct {
395395

396396
// LogFile is used by MonitorExport to stream a server's log file
397397
LogFile string `hcl:"log_file"`
398+
399+
CustomResources structs.CustomResources
398400
}
399401

400402
type APIListenerRegistrar interface {
@@ -896,6 +898,7 @@ func (c *Config) Copy() *Config {
896898
nc.ReservableCores = slices.Clone(c.ReservableCores)
897899
nc.Artifact = c.Artifact.Copy()
898900
nc.Users = c.Users.Copy()
901+
nc.CustomResources = c.CustomResources.Copy()
899902
return &nc
900903
}
901904

command/agent/agent.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ func NewAgent(config *Config, logger log.InterceptLogger, logOutput io.Writer, i
158158
if err := a.setupServer(); err != nil {
159159
return nil, err
160160
}
161+
161162
if err := a.setupClient(); err != nil {
162163
return nil, err
163164
}
@@ -936,6 +937,7 @@ func convertClientConfig(agentConfig *Config) (*clientconfig.Config, error) {
936937
conf.Node.Meta = agentConfig.Client.Meta
937938
conf.Node.NodeClass = agentConfig.Client.NodeClass
938939
conf.Node.NodePool = agentConfig.Client.NodePool
940+
conf.CustomResources = agentConfig.Client.CustomResources
939941

940942
// Set up the HTTP advertise address
941943
conf.Node.HTTPAddr = agentConfig.AdvertiseAddrs.HTTP

command/agent/config.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,10 @@ type ClientConfig struct {
443443

444444
// LogFile is used by MonitorExport to stream a client's log file
445445
LogFile string `hcl:"log_file"`
446+
447+
// CustomResources allows the user to define custom schedulable resources on
448+
// this node
449+
CustomResources structs.CustomResources `hcl:"custom_resource,block"`
446450
}
447451

448452
func (c *ClientConfig) Copy() *ClientConfig {
@@ -466,6 +470,7 @@ func (c *ClientConfig) Copy() *ClientConfig {
466470
nc.Drain = c.Drain.Copy()
467471
nc.Users = c.Users.Copy()
468472
nc.ExtraKeysHCL = slices.Clone(c.ExtraKeysHCL)
473+
nc.CustomResources = c.CustomResources.Copy()
469474
return &nc
470475
}
471476

@@ -2883,6 +2888,10 @@ func (c *ClientConfig) Merge(b *ClientConfig) *ClientConfig {
28832888
result.IntroToken = b.IntroToken
28842889
}
28852890

2891+
if b.CustomResources != nil {
2892+
result.CustomResources.Merge(b.CustomResources)
2893+
}
2894+
28862895
return &result
28872896
}
28882897

command/agent/config_parse.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,11 @@ func extraKeys(c *Config) error {
330330
helper.RemoveEqualFold(&c.Client.ExtraKeysHCL, "host_network")
331331
}
332332

333+
for _, cr := range c.Client.CustomResources {
334+
helper.RemoveEqualFold(&c.Client.ExtraKeysHCL, "custom_resource")
335+
helper.RemoveEqualFold(&c.Client.ExtraKeysHCL, cr.Name)
336+
}
337+
333338
// Remove Template extra keys
334339
for _, t := range []string{"function_denylist", "disable_file_sandbox", "max_stale", "wait", "wait_bounds", "block_query_wait", "consul_retry", "vault_retry", "nomad_retry"} {
335340
helper.RemoveEqualFold(&c.Client.ExtraKeysHCL, t)

command/agent/job_endpoint.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1646,6 +1646,22 @@ func ApiResourcesToStructs(in *api.Resources) *structs.Resources {
16461646
out.SecretsMB = *in.SecretsMB
16471647
}
16481648

1649+
if len(in.Custom) > 0 {
1650+
out.Custom = []*structs.CustomResource{}
1651+
for _, apiCr := range in.Custom {
1652+
out.Custom = append(out.Custom, &structs.CustomResource{
1653+
Name: apiCr.Name,
1654+
Version: apiCr.Version,
1655+
Type: structs.CustomResourceType(apiCr.Type),
1656+
Scope: structs.CustomResourceScope(apiCr.Scope),
1657+
Quantity: apiCr.Quantity,
1658+
Range: apiCr.Range,
1659+
Items: apiCr.Items,
1660+
Constraints: ApiConstraintsToStructs(apiCr.Constraints),
1661+
})
1662+
}
1663+
}
1664+
16491665
return out
16501666
}
16511667

command/node_status.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,7 @@ func (c *NodeStatusCommand) formatNode(client *api.Client, node *api.Node) int {
569569
c.outputNodeNetworkInfo(node)
570570
c.outputNodeCSIVolumeInfo(client, node, runningAllocs)
571571
c.outputNodeDriverInfo(node)
572+
c.outputNodeCustomResourceInfo(node)
572573
}
573574

574575
// Emit node events
@@ -788,6 +789,35 @@ func (c *NodeStatusCommand) outputNodeDriverInfo(node *api.Node) {
788789
c.Ui.Output(formatList(nodeDrivers))
789790
}
790791

792+
func (c *NodeStatusCommand) outputNodeCustomResourceInfo(node *api.Node) {
793+
c.Ui.Output(c.Colorize().Color("\n[bold]Custom Resources"))
794+
795+
resources := node.NodeResources.Custom
796+
if len(resources) == 0 {
797+
c.Ui.Output("<none>")
798+
return
799+
}
800+
801+
out := make([]string, 0, len(resources)+1)
802+
out = append(out, "Resource|Version|Type|Available")
803+
804+
sort.Slice(resources, func(i int, j int) bool { return resources[i].Name < resources[j].Name })
805+
for _, resource := range resources {
806+
switch resource.Type {
807+
case api.CustomResourceTypeRatio, api.CustomResourceTypeCappedRatio,
808+
api.CustomResourceTypeCountable:
809+
out = append(out, fmt.Sprintf("%s|%d|%s|%d",
810+
resource.Name, resource.Version, resource.Type, resource.Quantity))
811+
812+
case api.CustomResourceTypeStaticInstance, api.CustomResourceTypeDynamicInstance:
813+
out = append(out, fmt.Sprintf("%s|%d|%s|%v",
814+
resource.Name, resource.Version, resource.Type, resource.Items))
815+
816+
}
817+
}
818+
c.Ui.Output(formatList(out))
819+
}
820+
791821
func (c *NodeStatusCommand) outputNodeStatusEvents(node *api.Node) {
792822
c.Ui.Output(c.Colorize().Color("\n[bold]Node Events"))
793823
c.outputNodeEvent(node.Events)

0 commit comments

Comments
 (0)