github.com/hhrutter/nomad@v0.6.0-rc2.0.20170723054333-80c4b03f0705/scheduler/propertyset.go (about) 1 package scheduler 2 3 import ( 4 "fmt" 5 6 memdb "github.com/hashicorp/go-memdb" 7 "github.com/hashicorp/nomad/nomad/structs" 8 ) 9 10 // propertySet is used to track the values used for a particular property. 11 type propertySet struct { 12 // ctx is used to lookup the plan and state 13 ctx Context 14 15 // jobID is the job we are operating on 16 jobID string 17 18 // taskGroup is optionally set if the constraint is for a task group 19 taskGroup string 20 21 // constraint is the constraint this property set is checking 22 constraint *structs.Constraint 23 24 // errorBuilding marks whether there was an error when building the property 25 // set 26 errorBuilding error 27 28 // existingValues is the set of values for the given property that have been 29 // used by pre-existing allocations. 30 existingValues map[string]struct{} 31 32 // proposedValues is the set of values for the given property that are used 33 // from proposed allocations. 34 proposedValues map[string]struct{} 35 36 // clearedValues is the set of values that are no longer being used by 37 // existingValues because of proposed stops. 38 clearedValues map[string]struct{} 39 } 40 41 // NewPropertySet returns a new property set used to guarantee unique property 42 // values for new allocation placements. 43 func NewPropertySet(ctx Context, job *structs.Job) *propertySet { 44 p := &propertySet{ 45 ctx: ctx, 46 jobID: job.ID, 47 existingValues: make(map[string]struct{}), 48 } 49 50 return p 51 } 52 53 // SetJobConstraint is used to parameterize the property set for a 54 // distinct_property constraint set at the job level. 55 func (p *propertySet) SetJobConstraint(constraint *structs.Constraint) { 56 // Store the constraint 57 p.constraint = constraint 58 p.populateExisting(constraint) 59 } 60 61 // SetTGConstraint is used to parameterize the property set for a 62 // distinct_property constraint set at the task group level. The inputs are the 63 // constraint and the task group name. 64 func (p *propertySet) SetTGConstraint(constraint *structs.Constraint, taskGroup string) { 65 // Store that this is for a task group 66 p.taskGroup = taskGroup 67 68 // Store the constraint 69 p.constraint = constraint 70 71 p.populateExisting(constraint) 72 } 73 74 // populateExisting is a helper shared when setting the constraint to populate 75 // the existing values. 76 func (p *propertySet) populateExisting(constraint *structs.Constraint) { 77 // Retrieve all previously placed allocations 78 ws := memdb.NewWatchSet() 79 allocs, err := p.ctx.State().AllocsByJob(ws, p.jobID, false) 80 if err != nil { 81 p.errorBuilding = fmt.Errorf("failed to get job's allocations: %v", err) 82 p.ctx.Logger().Printf("[ERR] scheduler.dynamic-constraint: %v", p.errorBuilding) 83 return 84 } 85 86 // Filter to the correct set of allocs 87 allocs = p.filterAllocs(allocs, true) 88 89 // Get all the nodes that have been used by the allocs 90 nodes, err := p.buildNodeMap(allocs) 91 if err != nil { 92 p.errorBuilding = err 93 p.ctx.Logger().Printf("[ERR] scheduler.dynamic-constraint: %v", err) 94 return 95 } 96 97 // Build existing properties map 98 p.populateProperties(allocs, nodes, p.existingValues) 99 } 100 101 // PopulateProposed populates the proposed values and recomputes any cleared 102 // value. It should be called whenever the plan is updated to ensure correct 103 // results when checking an option. 104 func (p *propertySet) PopulateProposed() { 105 106 // Reset the proposed properties 107 p.proposedValues = make(map[string]struct{}) 108 p.clearedValues = make(map[string]struct{}) 109 110 // Gather the set of proposed stops. 111 var stopping []*structs.Allocation 112 for _, updates := range p.ctx.Plan().NodeUpdate { 113 stopping = append(stopping, updates...) 114 } 115 stopping = p.filterAllocs(stopping, false) 116 117 // Gather the proposed allocations 118 var proposed []*structs.Allocation 119 for _, pallocs := range p.ctx.Plan().NodeAllocation { 120 proposed = append(proposed, pallocs...) 121 } 122 proposed = p.filterAllocs(proposed, true) 123 124 // Get the used nodes 125 both := make([]*structs.Allocation, 0, len(stopping)+len(proposed)) 126 both = append(both, stopping...) 127 both = append(both, proposed...) 128 nodes, err := p.buildNodeMap(both) 129 if err != nil { 130 p.errorBuilding = err 131 p.ctx.Logger().Printf("[ERR] scheduler.dynamic-constraint: %v", err) 132 return 133 } 134 135 // Populate the cleared values 136 p.populateProperties(stopping, nodes, p.clearedValues) 137 138 // Populate the proposed values 139 p.populateProperties(proposed, nodes, p.proposedValues) 140 141 // Remove any cleared value that is now being used by the proposed allocs 142 for value := range p.proposedValues { 143 delete(p.clearedValues, value) 144 } 145 } 146 147 // SatisfiesDistinctProperties checks if the option satisfies the 148 // distinct_property constraints given the existing placements and proposed 149 // placements. If the option does not satisfy the constraints an explanation is 150 // given. 151 func (p *propertySet) SatisfiesDistinctProperties(option *structs.Node, tg string) (bool, string) { 152 // Check if there was an error building 153 if p.errorBuilding != nil { 154 return false, p.errorBuilding.Error() 155 } 156 157 // Get the nodes property value 158 nValue, ok := getProperty(option, p.constraint.LTarget) 159 if !ok { 160 return false, fmt.Sprintf("missing property %q", p.constraint.LTarget) 161 } 162 163 // both is used to iterate over both the proposed and existing used 164 // properties 165 bothAll := []map[string]struct{}{p.existingValues, p.proposedValues} 166 167 // Check if the nodes value has already been used. 168 for _, usedProperties := range bothAll { 169 // Check if the nodes value has been used 170 _, used := usedProperties[nValue] 171 if !used { 172 continue 173 } 174 175 // Check if the value has been cleared from a proposed stop 176 if _, cleared := p.clearedValues[nValue]; cleared { 177 continue 178 } 179 180 return false, fmt.Sprintf("distinct_property: %s=%s already used", p.constraint.LTarget, nValue) 181 } 182 183 return true, "" 184 } 185 186 // filterAllocs filters a set of allocations to just be those that are running 187 // and if the property set is operation at a task group level, for allocations 188 // for that task group 189 func (p *propertySet) filterAllocs(allocs []*structs.Allocation, filterTerminal bool) []*structs.Allocation { 190 n := len(allocs) 191 for i := 0; i < n; i++ { 192 remove := false 193 if filterTerminal { 194 remove = allocs[i].TerminalStatus() 195 } 196 197 // If the constraint is on the task group filter the allocations to just 198 // those on the task group 199 if p.taskGroup != "" { 200 remove = remove || allocs[i].TaskGroup != p.taskGroup 201 } 202 203 if remove { 204 allocs[i], allocs[n-1] = allocs[n-1], nil 205 i-- 206 n-- 207 } 208 } 209 return allocs[:n] 210 } 211 212 // buildNodeMap takes a list of allocations and returns a map of the nodes used 213 // by those allocations 214 func (p *propertySet) buildNodeMap(allocs []*structs.Allocation) (map[string]*structs.Node, error) { 215 // Get all the nodes that have been used by the allocs 216 nodes := make(map[string]*structs.Node) 217 ws := memdb.NewWatchSet() 218 for _, alloc := range allocs { 219 if _, ok := nodes[alloc.NodeID]; ok { 220 continue 221 } 222 223 node, err := p.ctx.State().NodeByID(ws, alloc.NodeID) 224 if err != nil { 225 return nil, fmt.Errorf("failed to lookup node ID %q: %v", alloc.NodeID, err) 226 } 227 228 nodes[alloc.NodeID] = node 229 } 230 231 return nodes, nil 232 } 233 234 // populateProperties goes through all allocations and builds up the used 235 // properties from the nodes storing the results in the passed properties map. 236 func (p *propertySet) populateProperties(allocs []*structs.Allocation, nodes map[string]*structs.Node, 237 properties map[string]struct{}) { 238 239 for _, alloc := range allocs { 240 nProperty, ok := getProperty(nodes[alloc.NodeID], p.constraint.LTarget) 241 if !ok { 242 continue 243 } 244 245 properties[nProperty] = struct{}{} 246 } 247 } 248 249 // getProperty is used to lookup the property value on the node 250 func getProperty(n *structs.Node, property string) (string, bool) { 251 if n == nil || property == "" { 252 return "", false 253 } 254 255 val, ok := resolveConstraintTarget(property, n) 256 if !ok { 257 return "", false 258 } 259 nodeValue, ok := val.(string) 260 if !ok { 261 return "", false 262 } 263 264 return nodeValue, true 265 }