github.com/ncodes/nomad@v0.5.7-0.20170403112158-97adf4a74fb3/scheduler/util.go (about) 1 package scheduler 2 3 import ( 4 "fmt" 5 "log" 6 "math/rand" 7 "reflect" 8 9 memdb "github.com/hashicorp/go-memdb" 10 "github.com/ncodes/nomad/nomad/structs" 11 ) 12 13 // allocTuple is a tuple of the allocation name and potential alloc ID 14 type allocTuple struct { 15 Name string 16 TaskGroup *structs.TaskGroup 17 Alloc *structs.Allocation 18 } 19 20 // materializeTaskGroups is used to materialize all the task groups 21 // a job requires. This is used to do the count expansion. 22 func materializeTaskGroups(job *structs.Job) map[string]*structs.TaskGroup { 23 out := make(map[string]*structs.TaskGroup) 24 if job == nil { 25 return out 26 } 27 28 for _, tg := range job.TaskGroups { 29 for i := 0; i < tg.Count; i++ { 30 name := fmt.Sprintf("%s.%s[%d]", job.Name, tg.Name, i) 31 out[name] = tg 32 } 33 } 34 return out 35 } 36 37 // diffResult is used to return the sets that result from the diff 38 type diffResult struct { 39 place, update, migrate, stop, ignore, lost []allocTuple 40 } 41 42 func (d *diffResult) GoString() string { 43 return fmt.Sprintf("allocs: (place %d) (update %d) (migrate %d) (stop %d) (ignore %d) (lost %d)", 44 len(d.place), len(d.update), len(d.migrate), len(d.stop), len(d.ignore), len(d.lost)) 45 } 46 47 func (d *diffResult) Append(other *diffResult) { 48 d.place = append(d.place, other.place...) 49 d.update = append(d.update, other.update...) 50 d.migrate = append(d.migrate, other.migrate...) 51 d.stop = append(d.stop, other.stop...) 52 d.ignore = append(d.ignore, other.ignore...) 53 d.lost = append(d.lost, other.lost...) 54 } 55 56 // diffAllocs is used to do a set difference between the target allocations 57 // and the existing allocations. This returns 6 sets of results, the list of 58 // named task groups that need to be placed (no existing allocation), the 59 // allocations that need to be updated (job definition is newer), allocs that 60 // need to be migrated (node is draining), the allocs that need to be evicted 61 // (no longer required), those that should be ignored and those that are lost 62 // that need to be replaced (running on a lost node). 63 // 64 // job is the job whose allocs is going to be diff-ed. 65 // taintedNodes is an index of the nodes which are either down or in drain mode 66 // by name. 67 // required is a set of allocations that must exist. 68 // allocs is a list of non terminal allocations. 69 // terminalAllocs is an index of the latest terminal allocations by name. 70 func diffAllocs(job *structs.Job, taintedNodes map[string]*structs.Node, 71 required map[string]*structs.TaskGroup, allocs []*structs.Allocation, 72 terminalAllocs map[string]*structs.Allocation) *diffResult { 73 result := &diffResult{} 74 75 // Scan the existing updates 76 existing := make(map[string]struct{}) 77 for _, exist := range allocs { 78 // Index the existing node 79 name := exist.Name 80 existing[name] = struct{}{} 81 82 // Check for the definition in the required set 83 tg, ok := required[name] 84 85 // If not required, we stop the alloc 86 if !ok { 87 result.stop = append(result.stop, allocTuple{ 88 Name: name, 89 TaskGroup: tg, 90 Alloc: exist, 91 }) 92 continue 93 } 94 95 // If we are on a tainted node, we must migrate if we are a service or 96 // if the batch allocation did not finish 97 if node, ok := taintedNodes[exist.NodeID]; ok { 98 // If the job is batch and finished successfully, the fact that the 99 // node is tainted does not mean it should be migrated or marked as 100 // lost as the work was already successfully finished. However for 101 // service/system jobs, tasks should never complete. The check of 102 // batch type, defends against client bugs. 103 if exist.Job.Type == structs.JobTypeBatch && exist.RanSuccessfully() { 104 goto IGNORE 105 } 106 107 if node == nil || node.TerminalStatus() { 108 result.lost = append(result.lost, allocTuple{ 109 Name: name, 110 TaskGroup: tg, 111 Alloc: exist, 112 }) 113 } else { 114 // This is the drain case 115 result.migrate = append(result.migrate, allocTuple{ 116 Name: name, 117 TaskGroup: tg, 118 Alloc: exist, 119 }) 120 } 121 continue 122 } 123 124 // If the definition is updated we need to update 125 if job.JobModifyIndex != exist.Job.JobModifyIndex { 126 result.update = append(result.update, allocTuple{ 127 Name: name, 128 TaskGroup: tg, 129 Alloc: exist, 130 }) 131 continue 132 } 133 134 // Everything is up-to-date 135 IGNORE: 136 result.ignore = append(result.ignore, allocTuple{ 137 Name: name, 138 TaskGroup: tg, 139 Alloc: exist, 140 }) 141 } 142 143 // Scan the required groups 144 for name, tg := range required { 145 // Check for an existing allocation 146 _, ok := existing[name] 147 148 // Require a placement if no existing allocation. If there 149 // is an existing allocation, we would have checked for a potential 150 // update or ignore above. 151 if !ok { 152 result.place = append(result.place, allocTuple{ 153 Name: name, 154 TaskGroup: tg, 155 Alloc: terminalAllocs[name], 156 }) 157 } 158 } 159 return result 160 } 161 162 // diffSystemAllocs is like diffAllocs however, the allocations in the 163 // diffResult contain the specific nodeID they should be allocated on. 164 // 165 // job is the job whose allocs is going to be diff-ed. 166 // nodes is a list of nodes in ready state. 167 // taintedNodes is an index of the nodes which are either down or in drain mode 168 // by name. 169 // allocs is a list of non terminal allocations. 170 // terminalAllocs is an index of the latest terminal allocations by name. 171 func diffSystemAllocs(job *structs.Job, nodes []*structs.Node, taintedNodes map[string]*structs.Node, 172 allocs []*structs.Allocation, terminalAllocs map[string]*structs.Allocation) *diffResult { 173 174 // Build a mapping of nodes to all their allocs. 175 nodeAllocs := make(map[string][]*structs.Allocation, len(allocs)) 176 for _, alloc := range allocs { 177 nallocs := append(nodeAllocs[alloc.NodeID], alloc) 178 nodeAllocs[alloc.NodeID] = nallocs 179 } 180 181 for _, node := range nodes { 182 if _, ok := nodeAllocs[node.ID]; !ok { 183 nodeAllocs[node.ID] = nil 184 } 185 } 186 187 // Create the required task groups. 188 required := materializeTaskGroups(job) 189 190 result := &diffResult{} 191 for nodeID, allocs := range nodeAllocs { 192 diff := diffAllocs(job, taintedNodes, required, allocs, terminalAllocs) 193 194 // If the node is tainted there should be no placements made 195 if _, ok := taintedNodes[nodeID]; ok { 196 diff.place = nil 197 } else { 198 // Mark the alloc as being for a specific node. 199 for i := range diff.place { 200 alloc := &diff.place[i] 201 202 // If the new allocation isn't annotated with a previous allocation 203 // or if the previous allocation isn't from the same node then we 204 // annotate the allocTuple with a new Allocation 205 if alloc.Alloc == nil || alloc.Alloc.NodeID != nodeID { 206 alloc.Alloc = &structs.Allocation{NodeID: nodeID} 207 } 208 } 209 } 210 211 // Migrate does not apply to system jobs and instead should be marked as 212 // stop because if a node is tainted, the job is invalid on that node. 213 diff.stop = append(diff.stop, diff.migrate...) 214 diff.migrate = nil 215 216 result.Append(diff) 217 } 218 219 return result 220 } 221 222 // readyNodesInDCs returns all the ready nodes in the given datacenters and a 223 // mapping of each data center to the count of ready nodes. 224 func readyNodesInDCs(state State, dcs []string) ([]*structs.Node, map[string]int, error) { 225 // Index the DCs 226 dcMap := make(map[string]int, len(dcs)) 227 for _, dc := range dcs { 228 dcMap[dc] = 0 229 } 230 231 // Scan the nodes 232 ws := memdb.NewWatchSet() 233 var out []*structs.Node 234 iter, err := state.Nodes(ws) 235 if err != nil { 236 return nil, nil, err 237 } 238 for { 239 raw := iter.Next() 240 if raw == nil { 241 break 242 } 243 244 // Filter on datacenter and status 245 node := raw.(*structs.Node) 246 if node.Status != structs.NodeStatusReady { 247 continue 248 } 249 if node.Drain { 250 continue 251 } 252 if _, ok := dcMap[node.Datacenter]; !ok { 253 continue 254 } 255 out = append(out, node) 256 dcMap[node.Datacenter] += 1 257 } 258 return out, dcMap, nil 259 } 260 261 // retryMax is used to retry a callback until it returns success or 262 // a maximum number of attempts is reached. An optional reset function may be 263 // passed which is called after each failed iteration. If the reset function is 264 // set and returns true, the number of attempts is reset back to max. 265 func retryMax(max int, cb func() (bool, error), reset func() bool) error { 266 attempts := 0 267 for attempts < max { 268 done, err := cb() 269 if err != nil { 270 return err 271 } 272 if done { 273 return nil 274 } 275 276 // Check if we should reset the number attempts 277 if reset != nil && reset() { 278 attempts = 0 279 } else { 280 attempts += 1 281 } 282 } 283 return &SetStatusError{ 284 Err: fmt.Errorf("maximum attempts reached (%d)", max), 285 EvalStatus: structs.EvalStatusFailed, 286 } 287 } 288 289 // progressMade checks to see if the plan result made allocations or updates. 290 // If the result is nil, false is returned. 291 func progressMade(result *structs.PlanResult) bool { 292 return result != nil && (len(result.NodeUpdate) != 0 || 293 len(result.NodeAllocation) != 0) 294 } 295 296 // taintedNodes is used to scan the allocations and then check if the 297 // underlying nodes are tainted, and should force a migration of the allocation. 298 // All the nodes returned in the map are tainted. 299 func taintedNodes(state State, allocs []*structs.Allocation) (map[string]*structs.Node, error) { 300 out := make(map[string]*structs.Node) 301 for _, alloc := range allocs { 302 if _, ok := out[alloc.NodeID]; ok { 303 continue 304 } 305 306 ws := memdb.NewWatchSet() 307 node, err := state.NodeByID(ws, alloc.NodeID) 308 if err != nil { 309 return nil, err 310 } 311 312 // If the node does not exist, we should migrate 313 if node == nil { 314 out[alloc.NodeID] = nil 315 continue 316 } 317 if structs.ShouldDrainNode(node.Status) || node.Drain { 318 out[alloc.NodeID] = node 319 } 320 } 321 return out, nil 322 } 323 324 // shuffleNodes randomizes the slice order with the Fisher-Yates algorithm 325 func shuffleNodes(nodes []*structs.Node) { 326 n := len(nodes) 327 for i := n - 1; i > 0; i-- { 328 j := rand.Intn(i + 1) 329 nodes[i], nodes[j] = nodes[j], nodes[i] 330 } 331 } 332 333 // tasksUpdated does a diff between task groups to see if the 334 // tasks, their drivers, environment variables or config have updated. The 335 // inputs are the task group name to diff and two jobs to diff. 336 func tasksUpdated(jobA, jobB *structs.Job, taskGroup string) bool { 337 a := jobA.LookupTaskGroup(taskGroup) 338 b := jobB.LookupTaskGroup(taskGroup) 339 340 // If the number of tasks do not match, clearly there is an update 341 if len(a.Tasks) != len(b.Tasks) { 342 return true 343 } 344 345 // Check ephemeral disk 346 if !reflect.DeepEqual(a.EphemeralDisk, b.EphemeralDisk) { 347 return true 348 } 349 350 // Check each task 351 for _, at := range a.Tasks { 352 bt := b.LookupTask(at.Name) 353 if bt == nil { 354 return true 355 } 356 if at.Driver != bt.Driver { 357 return true 358 } 359 if at.User != bt.User { 360 return true 361 } 362 if !reflect.DeepEqual(at.Config, bt.Config) { 363 return true 364 } 365 if !reflect.DeepEqual(at.Env, bt.Env) { 366 return true 367 } 368 if !reflect.DeepEqual(at.Artifacts, bt.Artifacts) { 369 return true 370 } 371 if !reflect.DeepEqual(at.Vault, bt.Vault) { 372 return true 373 } 374 if !reflect.DeepEqual(at.Templates, bt.Templates) { 375 return true 376 } 377 378 // Check the metadata 379 if !reflect.DeepEqual( 380 jobA.CombinedTaskMeta(taskGroup, at.Name), 381 jobB.CombinedTaskMeta(taskGroup, bt.Name)) { 382 return true 383 } 384 385 // Inspect the network to see if the dynamic ports are different 386 if len(at.Resources.Networks) != len(bt.Resources.Networks) { 387 return true 388 } 389 for idx := range at.Resources.Networks { 390 an := at.Resources.Networks[idx] 391 bn := bt.Resources.Networks[idx] 392 393 if an.MBits != bn.MBits { 394 return true 395 } 396 397 aPorts, bPorts := networkPortMap(an), networkPortMap(bn) 398 if !reflect.DeepEqual(aPorts, bPorts) { 399 return true 400 } 401 } 402 403 // Inspect the non-network resources 404 if ar, br := at.Resources, bt.Resources; ar.CPU != br.CPU { 405 return true 406 } else if ar.MemoryMB != br.MemoryMB { 407 return true 408 } else if ar.IOPS != br.IOPS { 409 return true 410 } 411 } 412 return false 413 } 414 415 // networkPortMap takes a network resource and returns a map of port labels to 416 // values. The value for dynamic ports is disregarded even if it is set. This 417 // makes this function suitable for comparing two network resources for changes. 418 func networkPortMap(n *structs.NetworkResource) map[string]int { 419 m := make(map[string]int, len(n.DynamicPorts)+len(n.ReservedPorts)) 420 for _, p := range n.ReservedPorts { 421 m[p.Label] = p.Value 422 } 423 for _, p := range n.DynamicPorts { 424 m[p.Label] = -1 425 } 426 return m 427 } 428 429 // setStatus is used to update the status of the evaluation 430 func setStatus(logger *log.Logger, planner Planner, 431 eval, nextEval, spawnedBlocked *structs.Evaluation, 432 tgMetrics map[string]*structs.AllocMetric, status, desc string, 433 queuedAllocs map[string]int) error { 434 435 logger.Printf("[DEBUG] sched: %#v: setting status to %s", eval, status) 436 newEval := eval.Copy() 437 newEval.Status = status 438 newEval.StatusDescription = desc 439 newEval.FailedTGAllocs = tgMetrics 440 if nextEval != nil { 441 newEval.NextEval = nextEval.ID 442 } 443 if spawnedBlocked != nil { 444 newEval.BlockedEval = spawnedBlocked.ID 445 } 446 if queuedAllocs != nil { 447 newEval.QueuedAllocations = queuedAllocs 448 } 449 450 return planner.UpdateEval(newEval) 451 } 452 453 // inplaceUpdate attempts to update allocations in-place where possible. It 454 // returns the allocs that couldn't be done inplace and then those that could. 455 func inplaceUpdate(ctx Context, eval *structs.Evaluation, job *structs.Job, 456 stack Stack, updates []allocTuple) (destructive, inplace []allocTuple) { 457 458 // doInplace manipulates the updates map to make the current allocation 459 // an inplace update. 460 doInplace := func(cur, last, inplaceCount *int) { 461 updates[*cur], updates[*last-1] = updates[*last-1], updates[*cur] 462 *cur-- 463 *last-- 464 *inplaceCount++ 465 } 466 467 ws := memdb.NewWatchSet() 468 n := len(updates) 469 inplaceCount := 0 470 for i := 0; i < n; i++ { 471 // Get the update 472 update := updates[i] 473 474 // Check if the task drivers or config has changed, requires 475 // a rolling upgrade since that cannot be done in-place. 476 existing := update.Alloc.Job 477 if tasksUpdated(job, existing, update.TaskGroup.Name) { 478 continue 479 } 480 481 // Terminal batch allocations are not filtered when they are completed 482 // successfully. We should avoid adding the allocation to the plan in 483 // the case that it is an in-place update to avoid both additional data 484 // in the plan and work for the clients. 485 if update.Alloc.TerminalStatus() { 486 doInplace(&i, &n, &inplaceCount) 487 continue 488 } 489 490 // Get the existing node 491 node, err := ctx.State().NodeByID(ws, update.Alloc.NodeID) 492 if err != nil { 493 ctx.Logger().Printf("[ERR] sched: %#v failed to get node '%s': %v", 494 eval, update.Alloc.NodeID, err) 495 continue 496 } 497 if node == nil { 498 continue 499 } 500 501 // Set the existing node as the base set 502 stack.SetNodes([]*structs.Node{node}) 503 504 // Stage an eviction of the current allocation. This is done so that 505 // the current allocation is discounted when checking for feasability. 506 // Otherwise we would be trying to fit the tasks current resources and 507 // updated resources. After select is called we can remove the evict. 508 ctx.Plan().AppendUpdate(update.Alloc, structs.AllocDesiredStatusStop, 509 allocInPlace, "") 510 511 // Attempt to match the task group 512 option, _ := stack.Select(update.TaskGroup) 513 514 // Pop the allocation 515 ctx.Plan().PopUpdate(update.Alloc) 516 517 // Skip if we could not do an in-place update 518 if option == nil { 519 continue 520 } 521 522 // Restore the network offers from the existing allocation. 523 // We do not allow network resources (reserved/dynamic ports) 524 // to be updated. This is guarded in taskUpdated, so we can 525 // safely restore those here. 526 for task, resources := range option.TaskResources { 527 existing := update.Alloc.TaskResources[task] 528 resources.Networks = existing.Networks 529 } 530 531 // Create a shallow copy 532 newAlloc := new(structs.Allocation) 533 *newAlloc = *update.Alloc 534 535 // Update the allocation 536 newAlloc.EvalID = eval.ID 537 newAlloc.Job = nil // Use the Job in the Plan 538 newAlloc.Resources = nil // Computed in Plan Apply 539 newAlloc.TaskResources = option.TaskResources 540 newAlloc.Metrics = ctx.Metrics() 541 ctx.Plan().AppendAlloc(newAlloc) 542 543 // Remove this allocation from the slice 544 doInplace(&i, &n, &inplaceCount) 545 } 546 547 if len(updates) > 0 { 548 ctx.Logger().Printf("[DEBUG] sched: %#v: %d in-place updates of %d", eval, inplaceCount, len(updates)) 549 } 550 return updates[:n], updates[n:] 551 } 552 553 // evictAndPlace is used to mark allocations for evicts and add them to the 554 // placement queue. evictAndPlace modifies both the diffResult and the 555 // limit. It returns true if the limit has been reached. 556 func evictAndPlace(ctx Context, diff *diffResult, allocs []allocTuple, desc string, limit *int) bool { 557 n := len(allocs) 558 for i := 0; i < n && i < *limit; i++ { 559 a := allocs[i] 560 ctx.Plan().AppendUpdate(a.Alloc, structs.AllocDesiredStatusStop, desc, "") 561 diff.place = append(diff.place, a) 562 } 563 if n <= *limit { 564 *limit -= n 565 return false 566 } 567 *limit = 0 568 return true 569 } 570 571 // markLostAndPlace is used to mark allocations as lost and add them to the 572 // placement queue. evictAndPlace modifies both the diffResult and the 573 // limit. It returns true if the limit has been reached. 574 func markLostAndPlace(ctx Context, diff *diffResult, allocs []allocTuple, desc string, limit *int) bool { 575 n := len(allocs) 576 for i := 0; i < n && i < *limit; i++ { 577 a := allocs[i] 578 ctx.Plan().AppendUpdate(a.Alloc, structs.AllocDesiredStatusStop, desc, structs.AllocClientStatusLost) 579 diff.place = append(diff.place, a) 580 } 581 if n <= *limit { 582 *limit -= n 583 return false 584 } 585 *limit = 0 586 return true 587 } 588 589 // tgConstrainTuple is used to store the total constraints of a task group. 590 type tgConstrainTuple struct { 591 // Holds the combined constraints of the task group and all it's sub-tasks. 592 constraints []*structs.Constraint 593 594 // The set of required drivers within the task group. 595 drivers map[string]struct{} 596 597 // The combined resources of all tasks within the task group. 598 size *structs.Resources 599 } 600 601 // taskGroupConstraints collects the constraints, drivers and resources required by each 602 // sub-task to aggregate the TaskGroup totals 603 func taskGroupConstraints(tg *structs.TaskGroup) tgConstrainTuple { 604 c := tgConstrainTuple{ 605 constraints: make([]*structs.Constraint, 0, len(tg.Constraints)), 606 drivers: make(map[string]struct{}), 607 size: &structs.Resources{DiskMB: tg.EphemeralDisk.SizeMB}, 608 } 609 610 c.constraints = append(c.constraints, tg.Constraints...) 611 for _, task := range tg.Tasks { 612 c.drivers[task.Driver] = struct{}{} 613 c.constraints = append(c.constraints, task.Constraints...) 614 c.size.Add(task.Resources) 615 } 616 617 return c 618 } 619 620 // desiredUpdates takes the diffResult as well as the set of inplace and 621 // destructive updates and returns a map of task groups to their set of desired 622 // updates. 623 func desiredUpdates(diff *diffResult, inplaceUpdates, 624 destructiveUpdates []allocTuple) map[string]*structs.DesiredUpdates { 625 desiredTgs := make(map[string]*structs.DesiredUpdates) 626 627 for _, tuple := range diff.place { 628 name := tuple.TaskGroup.Name 629 des, ok := desiredTgs[name] 630 if !ok { 631 des = &structs.DesiredUpdates{} 632 desiredTgs[name] = des 633 } 634 635 des.Place++ 636 } 637 638 for _, tuple := range diff.stop { 639 name := tuple.Alloc.TaskGroup 640 des, ok := desiredTgs[name] 641 if !ok { 642 des = &structs.DesiredUpdates{} 643 desiredTgs[name] = des 644 } 645 646 des.Stop++ 647 } 648 649 for _, tuple := range diff.ignore { 650 name := tuple.TaskGroup.Name 651 des, ok := desiredTgs[name] 652 if !ok { 653 des = &structs.DesiredUpdates{} 654 desiredTgs[name] = des 655 } 656 657 des.Ignore++ 658 } 659 660 for _, tuple := range diff.migrate { 661 name := tuple.TaskGroup.Name 662 des, ok := desiredTgs[name] 663 if !ok { 664 des = &structs.DesiredUpdates{} 665 desiredTgs[name] = des 666 } 667 668 des.Migrate++ 669 } 670 671 for _, tuple := range inplaceUpdates { 672 name := tuple.TaskGroup.Name 673 des, ok := desiredTgs[name] 674 if !ok { 675 des = &structs.DesiredUpdates{} 676 desiredTgs[name] = des 677 } 678 679 des.InPlaceUpdate++ 680 } 681 682 for _, tuple := range destructiveUpdates { 683 name := tuple.TaskGroup.Name 684 des, ok := desiredTgs[name] 685 if !ok { 686 des = &structs.DesiredUpdates{} 687 desiredTgs[name] = des 688 } 689 690 des.DestructiveUpdate++ 691 } 692 693 return desiredTgs 694 } 695 696 // adjustQueuedAllocations decrements the number of allocations pending per task 697 // group based on the number of allocations successfully placed 698 func adjustQueuedAllocations(logger *log.Logger, result *structs.PlanResult, queuedAllocs map[string]int) { 699 if result != nil { 700 for _, allocations := range result.NodeAllocation { 701 for _, allocation := range allocations { 702 // Ensure that the allocation is newly created. We check that 703 // the CreateIndex is equal to the ModifyIndex in order to check 704 // that the allocation was just created. We do not check that 705 // the CreateIndex is equal to the results AllocIndex because 706 // the allocations we get back have gone through the planner's 707 // optimistic snapshot and thus their indexes may not be 708 // correct, but they will be consistent. 709 if allocation.CreateIndex != allocation.ModifyIndex { 710 continue 711 } 712 713 if _, ok := queuedAllocs[allocation.TaskGroup]; ok { 714 queuedAllocs[allocation.TaskGroup] -= 1 715 } else { 716 logger.Printf("[ERR] sched: allocation %q placed but not in list of unplaced allocations", allocation.TaskGroup) 717 } 718 } 719 } 720 } 721 } 722 723 // updateNonTerminalAllocsToLost updates the allocations which are in pending/running state on tainted node 724 // to lost 725 func updateNonTerminalAllocsToLost(plan *structs.Plan, tainted map[string]*structs.Node, allocs []*structs.Allocation) { 726 for _, alloc := range allocs { 727 if _, ok := tainted[alloc.NodeID]; ok && 728 alloc.DesiredStatus == structs.AllocDesiredStatusStop && 729 (alloc.ClientStatus == structs.AllocClientStatusRunning || 730 alloc.ClientStatus == structs.AllocClientStatusPending) { 731 plan.AppendUpdate(alloc, structs.AllocDesiredStatusStop, allocLost, structs.AllocClientStatusLost) 732 } 733 } 734 }