github.com/zoomfoo/nomad@v0.8.5-0.20180907175415-f28fd3a1a056/scheduler/propertyset.go (about) 1 package scheduler 2 3 import ( 4 "fmt" 5 "strconv" 6 7 memdb "github.com/hashicorp/go-memdb" 8 "github.com/hashicorp/nomad/helper" 9 "github.com/hashicorp/nomad/nomad/structs" 10 ) 11 12 // propertySet is used to track the values used for a particular property. 13 type propertySet struct { 14 // ctx is used to lookup the plan and state 15 ctx Context 16 17 // jobID is the job we are operating on 18 jobID string 19 20 // namespace is the namespace of the job we are operating on 21 namespace string 22 23 // taskGroup is optionally set if the constraint is for a task group 24 taskGroup string 25 26 // targetAttribute is the attribute this property set is checking 27 targetAttribute string 28 29 // allowedCount is the allowed number of allocations that can have the 30 // distinct property 31 allowedCount uint64 32 33 // errorBuilding marks whether there was an error when building the property 34 // set 35 errorBuilding error 36 37 // existingValues is a mapping of the values of a property to the number of 38 // times the value has been used by pre-existing allocations. 39 existingValues map[string]uint64 40 41 // proposedValues is a mapping of the values of a property to the number of 42 // times the value has been used by proposed allocations. 43 proposedValues map[string]uint64 44 45 // clearedValues is a mapping of the values of a property to the number of 46 // times the value has been used by proposed stopped allocations. 47 clearedValues map[string]uint64 48 } 49 50 // NewPropertySet returns a new property set used to guarantee unique property 51 // values for new allocation placements. 52 func NewPropertySet(ctx Context, job *structs.Job) *propertySet { 53 p := &propertySet{ 54 ctx: ctx, 55 jobID: job.ID, 56 namespace: job.Namespace, 57 existingValues: make(map[string]uint64), 58 } 59 60 return p 61 } 62 63 // SetJobConstraint is used to parameterize the property set for a 64 // distinct_property constraint set at the job level. 65 func (p *propertySet) SetJobConstraint(constraint *structs.Constraint) { 66 p.setConstraint(constraint, "") 67 } 68 69 // SetTGConstraint is used to parameterize the property set for a 70 // distinct_property constraint set at the task group level. The inputs are the 71 // constraint and the task group name. 72 func (p *propertySet) SetTGConstraint(constraint *structs.Constraint, taskGroup string) { 73 p.setConstraint(constraint, taskGroup) 74 } 75 76 // setConstraint is a shared helper for setting a job or task group constraint. 77 func (p *propertySet) setConstraint(constraint *structs.Constraint, taskGroup string) { 78 var allowedCount uint64 79 // Determine the number of allowed allocations with the property. 80 if v := constraint.RTarget; v != "" { 81 c, err := strconv.ParseUint(v, 10, 64) 82 if err != nil { 83 p.errorBuilding = fmt.Errorf("failed to convert RTarget %q to uint64: %v", v, err) 84 p.ctx.Logger().Printf("[ERR] scheduler.dynamic-constraint: %v", p.errorBuilding) 85 return 86 } 87 88 allowedCount = c 89 } else { 90 allowedCount = 1 91 } 92 p.setTargetAttributeWithCount(constraint.LTarget, allowedCount, taskGroup) 93 } 94 95 // SetTargetAttribute is used to populate this property set without also storing allowed count 96 // This is used when evaluating spread stanzas 97 func (p *propertySet) SetTargetAttribute(targetAttribute string, taskGroup string) { 98 p.setTargetAttributeWithCount(targetAttribute, 0, taskGroup) 99 } 100 101 // setTargetAttributeWithCount is a shared helper for setting a job or task group attribute and allowedCount 102 // allowedCount can be zero when this is used in evaluating spread stanzas 103 func (p *propertySet) setTargetAttributeWithCount(targetAttribute string, allowedCount uint64, taskGroup string) { 104 // Store that this is for a task group 105 if taskGroup != "" { 106 p.taskGroup = taskGroup 107 } 108 109 // Store the constraint 110 p.targetAttribute = targetAttribute 111 112 p.allowedCount = allowedCount 113 114 // Determine the number of existing allocations that are using a property 115 // value 116 p.populateExisting() 117 118 // Populate the proposed when setting the constraint. We do this because 119 // when detecting if we can inplace update an allocation we stage an 120 // eviction and then select. This means the plan has an eviction before a 121 // single select has finished. 122 p.PopulateProposed() 123 } 124 125 // populateExisting is a helper shared when setting the constraint to populate 126 // the existing values. 127 func (p *propertySet) populateExisting() { 128 // Retrieve all previously placed allocations 129 ws := memdb.NewWatchSet() 130 allocs, err := p.ctx.State().AllocsByJob(ws, p.namespace, p.jobID, false) 131 if err != nil { 132 p.errorBuilding = fmt.Errorf("failed to get job's allocations: %v", err) 133 p.ctx.Logger().Printf("[ERR] scheduler.dynamic-constraint: %v", p.errorBuilding) 134 return 135 } 136 137 // Filter to the correct set of allocs 138 allocs = p.filterAllocs(allocs, true) 139 140 // Get all the nodes that have been used by the allocs 141 nodes, err := p.buildNodeMap(allocs) 142 if err != nil { 143 p.errorBuilding = err 144 p.ctx.Logger().Printf("[ERR] scheduler.dynamic-constraint: %v", err) 145 return 146 } 147 148 // Build existing properties map 149 p.populateProperties(allocs, nodes, p.existingValues) 150 } 151 152 // PopulateProposed populates the proposed values and recomputes any cleared 153 // value. It should be called whenever the plan is updated to ensure correct 154 // results when checking an option. 155 func (p *propertySet) PopulateProposed() { 156 157 // Reset the proposed properties 158 p.proposedValues = make(map[string]uint64) 159 p.clearedValues = make(map[string]uint64) 160 161 // Gather the set of proposed stops. 162 var stopping []*structs.Allocation 163 for _, updates := range p.ctx.Plan().NodeUpdate { 164 stopping = append(stopping, updates...) 165 } 166 stopping = p.filterAllocs(stopping, false) 167 168 // Gather the proposed allocations 169 var proposed []*structs.Allocation 170 for _, pallocs := range p.ctx.Plan().NodeAllocation { 171 proposed = append(proposed, pallocs...) 172 } 173 proposed = p.filterAllocs(proposed, true) 174 175 // Get the used nodes 176 both := make([]*structs.Allocation, 0, len(stopping)+len(proposed)) 177 both = append(both, stopping...) 178 both = append(both, proposed...) 179 nodes, err := p.buildNodeMap(both) 180 if err != nil { 181 p.errorBuilding = err 182 p.ctx.Logger().Printf("[ERR] scheduler.dynamic-constraint: %v", err) 183 return 184 } 185 186 // Populate the cleared values 187 p.populateProperties(stopping, nodes, p.clearedValues) 188 189 // Populate the proposed values 190 p.populateProperties(proposed, nodes, p.proposedValues) 191 192 // Remove any cleared value that is now being used by the proposed allocs 193 for value := range p.proposedValues { 194 current, ok := p.clearedValues[value] 195 if !ok { 196 continue 197 } else if current == 0 { 198 delete(p.clearedValues, value) 199 } else if current > 1 { 200 p.clearedValues[value]-- 201 } 202 } 203 } 204 205 // SatisfiesDistinctProperties checks if the option satisfies the 206 // distinct_property constraints given the existing placements and proposed 207 // placements. If the option does not satisfy the constraints an explanation is 208 // given. 209 func (p *propertySet) SatisfiesDistinctProperties(option *structs.Node, tg string) (bool, string) { 210 nValue, errorMsg, usedCount := p.UsedCount(option, tg) 211 if errorMsg != "" { 212 return false, errorMsg 213 } 214 // The property value has been used but within the number of allowed 215 // allocations. 216 if usedCount < p.allowedCount { 217 return true, "" 218 } 219 220 return false, fmt.Sprintf("distinct_property: %s=%s used by %d allocs", p.targetAttribute, nValue, usedCount) 221 } 222 223 // UsedCount returns the number of times the value of the attribute being tracked by this 224 // property set is used across current and proposed allocations. It also returns the resolved 225 // attribute value for the node, and an error message if it couldn't be resolved correctly 226 func (p *propertySet) UsedCount(option *structs.Node, tg string) (string, string, uint64) { 227 // Check if there was an error building 228 if p.errorBuilding != nil { 229 return "", p.errorBuilding.Error(), 0 230 } 231 232 // Get the nodes property value 233 nValue, ok := getProperty(option, p.targetAttribute) 234 if !ok { 235 return nValue, fmt.Sprintf("missing property %q", p.targetAttribute), 0 236 } 237 combinedUse := p.GetCombinedUseMap() 238 usedCount := combinedUse[nValue] 239 return nValue, "", usedCount 240 } 241 242 // GetCombinedUseMap counts how many times the property has been used by 243 // existing and proposed allocations. It also takes into account any stopped 244 // allocations 245 func (p *propertySet) GetCombinedUseMap() map[string]uint64 { 246 combinedUse := make(map[string]uint64, helper.IntMax(len(p.existingValues), len(p.proposedValues))) 247 for _, usedValues := range []map[string]uint64{p.existingValues, p.proposedValues} { 248 for propertyValue, usedCount := range usedValues { 249 combinedUse[propertyValue] += usedCount 250 } 251 } 252 253 // Go through and discount the combined count when the value has been 254 // cleared by a proposed stop. 255 for propertyValue, clearedCount := range p.clearedValues { 256 combined, ok := combinedUse[propertyValue] 257 if !ok { 258 continue 259 } 260 261 // Don't clear below 0. 262 if combined >= clearedCount { 263 combinedUse[propertyValue] = combined - clearedCount 264 } else { 265 combinedUse[propertyValue] = 0 266 } 267 } 268 return combinedUse 269 } 270 271 // filterAllocs filters a set of allocations to just be those that are running 272 // and if the property set is operation at a task group level, for allocations 273 // for that task group 274 func (p *propertySet) filterAllocs(allocs []*structs.Allocation, filterTerminal bool) []*structs.Allocation { 275 n := len(allocs) 276 for i := 0; i < n; i++ { 277 remove := false 278 if filterTerminal { 279 remove = allocs[i].TerminalStatus() 280 } 281 282 // If the constraint is on the task group filter the allocations to just 283 // those on the task group 284 if p.taskGroup != "" { 285 remove = remove || allocs[i].TaskGroup != p.taskGroup 286 } 287 288 if remove { 289 allocs[i], allocs[n-1] = allocs[n-1], nil 290 i-- 291 n-- 292 } 293 } 294 return allocs[:n] 295 } 296 297 // buildNodeMap takes a list of allocations and returns a map of the nodes used 298 // by those allocations 299 func (p *propertySet) buildNodeMap(allocs []*structs.Allocation) (map[string]*structs.Node, error) { 300 // Get all the nodes that have been used by the allocs 301 nodes := make(map[string]*structs.Node) 302 ws := memdb.NewWatchSet() 303 for _, alloc := range allocs { 304 if _, ok := nodes[alloc.NodeID]; ok { 305 continue 306 } 307 308 node, err := p.ctx.State().NodeByID(ws, alloc.NodeID) 309 if err != nil { 310 return nil, fmt.Errorf("failed to lookup node ID %q: %v", alloc.NodeID, err) 311 } 312 313 nodes[alloc.NodeID] = node 314 } 315 316 return nodes, nil 317 } 318 319 // populateProperties goes through all allocations and builds up the used 320 // properties from the nodes storing the results in the passed properties map. 321 func (p *propertySet) populateProperties(allocs []*structs.Allocation, nodes map[string]*structs.Node, 322 properties map[string]uint64) { 323 324 for _, alloc := range allocs { 325 nProperty, ok := getProperty(nodes[alloc.NodeID], p.targetAttribute) 326 if !ok { 327 continue 328 } 329 330 properties[nProperty]++ 331 } 332 } 333 334 // getProperty is used to lookup the property value on the node 335 func getProperty(n *structs.Node, property string) (string, bool) { 336 if n == nil || property == "" { 337 return "", false 338 } 339 340 val, ok := resolveTarget(property, n) 341 if !ok { 342 return "", false 343 } 344 nodeValue, ok := val.(string) 345 if !ok { 346 return "", false 347 } 348 349 return nodeValue, true 350 }