github.com/zoomfoo/nomad@v0.8.5-0.20180907175415-f28fd3a1a056/scheduler/context.go (about) 1 package scheduler 2 3 import ( 4 "log" 5 "regexp" 6 7 memdb "github.com/hashicorp/go-memdb" 8 "github.com/hashicorp/go-version" 9 "github.com/hashicorp/nomad/nomad/structs" 10 ) 11 12 // Context is used to track contextual information used for placement 13 type Context interface { 14 // State is used to inspect the current global state 15 State() State 16 17 // Plan returns the current plan 18 Plan() *structs.Plan 19 20 // Logger provides a way to log 21 Logger() *log.Logger 22 23 // Metrics returns the current metrics 24 Metrics() *structs.AllocMetric 25 26 // Reset is invoked after making a placement 27 Reset() 28 29 // ProposedAllocs returns the proposed allocations for a node 30 // which is the existing allocations, removing evictions, and 31 // adding any planned placements. 32 ProposedAllocs(nodeID string) ([]*structs.Allocation, error) 33 34 // RegexpCache is a cache of regular expressions 35 RegexpCache() map[string]*regexp.Regexp 36 37 // VersionConstraintCache is a cache of version constraints 38 VersionConstraintCache() map[string]version.Constraints 39 40 // Eligibility returns a tracker for node eligibility in the context of the 41 // eval. 42 Eligibility() *EvalEligibility 43 } 44 45 // EvalCache is used to cache certain things during an evaluation 46 type EvalCache struct { 47 reCache map[string]*regexp.Regexp 48 constraintCache map[string]version.Constraints 49 } 50 51 func (e *EvalCache) RegexpCache() map[string]*regexp.Regexp { 52 if e.reCache == nil { 53 e.reCache = make(map[string]*regexp.Regexp) 54 } 55 return e.reCache 56 } 57 58 func (e *EvalCache) VersionConstraintCache() map[string]version.Constraints { 59 if e.constraintCache == nil { 60 e.constraintCache = make(map[string]version.Constraints) 61 } 62 return e.constraintCache 63 } 64 65 // EvalContext is a Context used during an Evaluation 66 type EvalContext struct { 67 EvalCache 68 state State 69 plan *structs.Plan 70 logger *log.Logger 71 metrics *structs.AllocMetric 72 eligibility *EvalEligibility 73 } 74 75 // NewEvalContext constructs a new EvalContext 76 func NewEvalContext(s State, p *structs.Plan, log *log.Logger) *EvalContext { 77 ctx := &EvalContext{ 78 state: s, 79 plan: p, 80 logger: log, 81 metrics: new(structs.AllocMetric), 82 } 83 return ctx 84 } 85 86 func (e *EvalContext) State() State { 87 return e.state 88 } 89 90 func (e *EvalContext) Plan() *structs.Plan { 91 return e.plan 92 } 93 94 func (e *EvalContext) Logger() *log.Logger { 95 return e.logger 96 } 97 98 func (e *EvalContext) Metrics() *structs.AllocMetric { 99 return e.metrics 100 } 101 102 func (e *EvalContext) SetState(s State) { 103 e.state = s 104 } 105 106 func (e *EvalContext) Reset() { 107 e.metrics = new(structs.AllocMetric) 108 } 109 110 func (e *EvalContext) ProposedAllocs(nodeID string) ([]*structs.Allocation, error) { 111 // Get the existing allocations that are non-terminal 112 ws := memdb.NewWatchSet() 113 existingAlloc, err := e.state.AllocsByNodeTerminal(ws, nodeID, false) 114 if err != nil { 115 return nil, err 116 } 117 118 // Determine the proposed allocation by first removing allocations 119 // that are planned evictions and adding the new allocations. 120 proposed := existingAlloc 121 if update := e.plan.NodeUpdate[nodeID]; len(update) > 0 { 122 proposed = structs.RemoveAllocs(existingAlloc, update) 123 } 124 125 // We create an index of the existing allocations so that if an inplace 126 // update occurs, we do not double count and we override the old allocation. 127 proposedIDs := make(map[string]*structs.Allocation, len(proposed)) 128 for _, alloc := range proposed { 129 proposedIDs[alloc.ID] = alloc 130 } 131 for _, alloc := range e.plan.NodeAllocation[nodeID] { 132 proposedIDs[alloc.ID] = alloc 133 } 134 135 // Materialize the proposed slice 136 proposed = make([]*structs.Allocation, 0, len(proposedIDs)) 137 for _, alloc := range proposedIDs { 138 proposed = append(proposed, alloc) 139 } 140 141 return proposed, nil 142 } 143 144 func (e *EvalContext) Eligibility() *EvalEligibility { 145 if e.eligibility == nil { 146 e.eligibility = NewEvalEligibility() 147 } 148 149 return e.eligibility 150 } 151 152 type ComputedClassFeasibility byte 153 154 const ( 155 // EvalComputedClassUnknown is the initial state until the eligibility has 156 // been explicitly marked to eligible/ineligible or escaped. 157 EvalComputedClassUnknown ComputedClassFeasibility = iota 158 159 // EvalComputedClassIneligible is used to mark the computed class as 160 // ineligible for the evaluation. 161 EvalComputedClassIneligible 162 163 // EvalComputedClassIneligible is used to mark the computed class as 164 // eligible for the evaluation. 165 EvalComputedClassEligible 166 167 // EvalComputedClassEscaped signals that computed class can not determine 168 // eligibility because a constraint exists that is not captured by computed 169 // node classes. 170 EvalComputedClassEscaped 171 ) 172 173 // EvalEligibility tracks eligibility of nodes by computed node class over the 174 // course of an evaluation. 175 type EvalEligibility struct { 176 // job tracks the eligibility at the job level per computed node class. 177 job map[string]ComputedClassFeasibility 178 179 // jobEscaped marks whether constraints have escaped at the job level. 180 jobEscaped bool 181 182 // taskGroups tracks the eligibility at the task group level per computed 183 // node class. 184 taskGroups map[string]map[string]ComputedClassFeasibility 185 186 // tgEscapedConstraints is a map of task groups to whether constraints have 187 // escaped. 188 tgEscapedConstraints map[string]bool 189 190 // quotaReached marks that the quota limit has been reached for the given 191 // quota 192 quotaReached string 193 } 194 195 // NewEvalEligibility returns an eligibility tracker for the context of an evaluation. 196 func NewEvalEligibility() *EvalEligibility { 197 return &EvalEligibility{ 198 job: make(map[string]ComputedClassFeasibility), 199 taskGroups: make(map[string]map[string]ComputedClassFeasibility), 200 tgEscapedConstraints: make(map[string]bool), 201 } 202 } 203 204 // SetJob takes the job being evaluated and calculates the escaped constraints 205 // at the job and task group level. 206 func (e *EvalEligibility) SetJob(job *structs.Job) { 207 // Determine whether the job has escaped constraints. 208 e.jobEscaped = len(structs.EscapedConstraints(job.Constraints)) != 0 209 210 // Determine the escaped constraints per task group. 211 for _, tg := range job.TaskGroups { 212 constraints := tg.Constraints 213 for _, task := range tg.Tasks { 214 constraints = append(constraints, task.Constraints...) 215 } 216 217 e.tgEscapedConstraints[tg.Name] = len(structs.EscapedConstraints(constraints)) != 0 218 } 219 } 220 221 // HasEscaped returns whether any of the constraints in the passed job have 222 // escaped computed node classes. 223 func (e *EvalEligibility) HasEscaped() bool { 224 if e.jobEscaped { 225 return true 226 } 227 228 for _, escaped := range e.tgEscapedConstraints { 229 if escaped { 230 return true 231 } 232 } 233 234 return false 235 } 236 237 // GetClasses returns the tracked classes to their eligibility, across the job 238 // and task groups. 239 func (e *EvalEligibility) GetClasses() map[string]bool { 240 elig := make(map[string]bool) 241 242 // Go through the job. 243 for class, feas := range e.job { 244 switch feas { 245 case EvalComputedClassEligible: 246 elig[class] = true 247 case EvalComputedClassIneligible: 248 elig[class] = false 249 } 250 } 251 252 // Go through the task groups. 253 for _, classes := range e.taskGroups { 254 for class, feas := range classes { 255 switch feas { 256 case EvalComputedClassEligible: 257 elig[class] = true 258 case EvalComputedClassIneligible: 259 // Only mark as ineligible if it hasn't been marked before. This 260 // prevents one task group marking a class as ineligible when it 261 // is eligible on another task group. 262 if _, ok := elig[class]; !ok { 263 elig[class] = false 264 } 265 } 266 } 267 } 268 269 return elig 270 } 271 272 // JobStatus returns the eligibility status of the job. 273 func (e *EvalEligibility) JobStatus(class string) ComputedClassFeasibility { 274 // COMPAT: Computed node class was introduced in 0.3. Clients running < 0.3 275 // will not have a computed class. The safest value to return is the escaped 276 // case, since it disables any optimization. 277 if e.jobEscaped || class == "" { 278 return EvalComputedClassEscaped 279 } 280 281 if status, ok := e.job[class]; ok { 282 return status 283 } 284 return EvalComputedClassUnknown 285 } 286 287 // SetJobEligibility sets the eligibility status of the job for the computed 288 // node class. 289 func (e *EvalEligibility) SetJobEligibility(eligible bool, class string) { 290 if eligible { 291 e.job[class] = EvalComputedClassEligible 292 } else { 293 e.job[class] = EvalComputedClassIneligible 294 } 295 } 296 297 // TaskGroupStatus returns the eligibility status of the task group. 298 func (e *EvalEligibility) TaskGroupStatus(tg, class string) ComputedClassFeasibility { 299 // COMPAT: Computed node class was introduced in 0.3. Clients running < 0.3 300 // will not have a computed class. The safest value to return is the escaped 301 // case, since it disables any optimization. 302 if class == "" { 303 return EvalComputedClassEscaped 304 } 305 306 if escaped, ok := e.tgEscapedConstraints[tg]; ok { 307 if escaped { 308 return EvalComputedClassEscaped 309 } 310 } 311 312 if classes, ok := e.taskGroups[tg]; ok { 313 if status, ok := classes[class]; ok { 314 return status 315 } 316 } 317 return EvalComputedClassUnknown 318 } 319 320 // SetTaskGroupEligibility sets the eligibility status of the task group for the 321 // computed node class. 322 func (e *EvalEligibility) SetTaskGroupEligibility(eligible bool, tg, class string) { 323 var eligibility ComputedClassFeasibility 324 if eligible { 325 eligibility = EvalComputedClassEligible 326 } else { 327 eligibility = EvalComputedClassIneligible 328 } 329 330 if classes, ok := e.taskGroups[tg]; ok { 331 classes[class] = eligibility 332 } else { 333 e.taskGroups[tg] = map[string]ComputedClassFeasibility{class: eligibility} 334 } 335 } 336 337 // SetQuotaLimitReached marks that the quota limit has been reached for the 338 // given quota 339 func (e *EvalEligibility) SetQuotaLimitReached(quota string) { 340 e.quotaReached = quota 341 } 342 343 // QuotaLimitReached returns the quota name if the quota limit has been reached. 344 func (e *EvalEligibility) QuotaLimitReached() string { 345 return e.quotaReached 346 }