// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package structs // DeviceAccounter is used to account for device usage on a node. It can detect // when a node is oversubscribed and can be used for deciding what devices are // free type DeviceAccounter struct { // Devices maps a device group to its device accounter instance Devices map[DeviceIdTuple]*DeviceAccounterInstance } // DeviceAccounterInstance wraps a device and adds tracking to the instances of // the device to determine if they are free or not. type DeviceAccounterInstance struct { // Device is the device being wrapped Device *NodeDeviceResource // Instances is a mapping of the device IDs to their usage. // Only a value of 0 indicates that the instance is unused. Instances map[string]int } // NewDeviceAccounter returns a new device accounter. The node is used to // populate the set of available devices based on what healthy device instances // exist on the node. func NewDeviceAccounter(n *Node) *DeviceAccounter { numDevices := 0 var devices []*NodeDeviceResource // COMPAT(0.11): Remove in 0.11 if n.NodeResources != nil { numDevices = len(n.NodeResources.Devices) devices = n.NodeResources.Devices } d := &DeviceAccounter{ Devices: make(map[DeviceIdTuple]*DeviceAccounterInstance, numDevices), } for _, dev := range devices { id := *dev.ID() d.Devices[id] = &DeviceAccounterInstance{ Device: dev, Instances: make(map[string]int, len(dev.Instances)), } for _, instance := range dev.Instances { // Skip unhealthy devices as they aren't allocatable if !instance.Healthy { continue } d.Devices[id].Instances[instance.ID] = 0 } } return d } // AddAllocs takes a set of allocations and internally marks which devices are // used. If a device is used more than once by the set of passed allocations, // the collision will be returned as true. func (d *DeviceAccounter) AddAllocs(allocs []*Allocation) (collision bool) { for _, a := range allocs { // Filter any terminal allocation if a.ClientTerminalStatus() { continue } // COMPAT(0.11): Remove in 0.11 // If the alloc doesn't have the new style resources, it can't have // devices if a.AllocatedResources == nil { continue } // Go through each task resource for _, tr := range a.AllocatedResources.Tasks { // Go through each assigned device group for _, device := range tr.Devices { devID := device.ID() // Go through each assigned device for _, instanceID := range device.DeviceIDs { // Mark that we are using the device. It may not be in the // map if the device is no longer being fingerprinted, is // unhealthy, etc. if devInst, ok := d.Devices[*devID]; ok { if i, ok := devInst.Instances[instanceID]; ok { // Mark that the device is in use devInst.Instances[instanceID]++ if i != 0 { collision = true } } } } } } } return } // AddReserved marks the device instances in the passed device reservation as // used and returns if there is a collision. func (d *DeviceAccounter) AddReserved(res *AllocatedDeviceResource) (collision bool) { // Lookup the device. devInst, ok := d.Devices[*res.ID()] if !ok { return false } // For each reserved instance, mark it as used for _, id := range res.DeviceIDs { cur, ok := devInst.Instances[id] if !ok { continue } // It has already been used, so mark that there is a collision if cur != 0 { collision = true } devInst.Instances[id]++ } return } // FreeCount returns the number of free device instances func (i *DeviceAccounterInstance) FreeCount() int { count := 0 for _, c := range i.Instances { if c == 0 { count++ } } return count }